Skip to content

Commit

Permalink
feat(vote-indicators): Added vote indicators
Browse files Browse the repository at this point in the history
feat(vote-indicators): Added vote indicators
mikedegeofroy authored Nov 25, 2024
2 parents f8b75fc + 6acffd3 commit f74a5af
Showing 12 changed files with 200 additions and 80 deletions.
6 changes: 6 additions & 0 deletions src/components/ui/game.tsx
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { Outlet } from 'react-router-dom';
import { useWebApp } from '@vkruglikov/react-telegram-web-app';

import sadFace from '@/assets/icons/sad-face.png';
import { Toaster } from 'react-hot-toast';


const gameScreenVariants = {
@@ -41,6 +42,11 @@ export const GameComponent = () => {

return (
<main className="h-screen mx-auto bg-background">
<Toaster
toastOptions={{
className: '!bg-secondary !text-foreground !rounded-xl !w-full',
}}
/>
{!isVersionAtLeast("7.2") ?
<div className="flex space-y-3 h-[95vh] items-center justify-center flex-col">
<div className="w-[30%] mx-auto pb-2">
40 changes: 34 additions & 6 deletions src/modules/game/card.tsx
Original file line number Diff line number Diff line change
@@ -46,25 +46,53 @@ export const CardComponent = ({ data, deltaY }: Props) => {
}
};

const [imageIndex, setImageIndex] = useState(0);

return (
<div className="relative h-full overflow-hidden rounded-3xl">
<div className="h-[420px] w-full xs:h-[420px]">
<div className="bg-slate-100 h-full w-full rounded-t-3xl pb-4 overflow-hidden">
<div
className="bg-slate-100 h-full w-full rounded-t-3xl pb-4 overflow-hidden">
<img
draggable="false"
className="h-full w-auto min-w-full object-cover"
src={data.images[0]}
src={data.images[imageIndex]}
/>
</div>
</div>
<div className="w-full absolute opacity-50 px-5 gap-4 flex p-2 top-0 justify-between">
{data.images.length > 1 && data.images.map((_, index) => {
return (
<div
key={`image_${index}`}
className={`${index == imageIndex ? 'bg-muted' : 'bg-muted-foreground'} h-[2px] w-full rounded-full`}
/>
)
})}
</div>
{deltaY &&
<div className="w-full absolute flex p-5 top-0 justify-between h-14">
<motion.div style={{ opacity: leftOpacity }} className="w-12 h-12 bg-white rounded-full"><img className="p-3" src={LikeIcon} /></motion.div>
<motion.div style={{ opacity: rightOpacity }} className="w-12 h-12 bg-white rounded-full"><img className="p-3" src={DislikeIcon} /></motion.div>
</div>}
<div className="absolute top-0 w-full h-full">
<motion.div
className="absolute pt-4 bottom-0 w-full rounded-3xl bg-secondary shadow-md overflow-hidden"
<motion.div
onTap={(e: MouseEvent) => {
const boundingBox = (e.target as HTMLElement).getBoundingClientRect();
const tapX = e.clientX - boundingBox.left;
const elementWidth = boundingBox.width;

setImageIndex((prevIndex) => {
if ((tapX + elementWidth / 4) < elementWidth / 2) {
return (prevIndex - 1 + data.images.length) % data.images.length;
} else if ((tapX - elementWidth / 4) > elementWidth / 2) {
return (prevIndex + 1) % data.images.length;
}
return prevIndex;
});
}}
className="absolute top-0 w-full h-full"
>
<motion.div className="absolute pt-4 bottom-0 w-full rounded-3xl bg-secondary shadow-md overflow-hidden"
initial={{ height: '43%' }}
animate={{ height: expanded ? '80%' : '43%' }}
transition={{ duration: 0.4, ease: [0.25, 0.8, 0.5, 1] }}
@@ -99,7 +127,7 @@ export const CardComponent = ({ data, deltaY }: Props) => {
</p>
</div>
</motion.div>
</div>
</motion.div>
</div >
);
};
110 changes: 74 additions & 36 deletions src/modules/game/match.card.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import { useMatchStore } from '@/shared/stores/match.store';
import { matchEvent } from '@/shared/events/app-events/match.event';
import { useWebApp } from '@vkruglikov/react-telegram-web-app';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { voteEvent } from '@/shared/events/app-events/vote.event';
import { useVoteStore } from '@/shared/stores/vote.store';
import { useLobbyStore } from '@/shared/stores/lobby.store';

const MatchCard = () => {
const { card } = useMatchStore();
const { card, id } = useMatchStore();
const { votes } = useVoteStore();
const { users } = useLobbyStore();

const webApp = useWebApp();
const { openLink } = webApp;

useEffect(() => {
const currentVotes = votes.filter(x => x.id == id);

const continueVotes = currentVotes.filter(x => x.option == 0);
const stopVotes = currentVotes.filter(x => x.option == 1);

webApp.MainButton.setText(`Продолжить (${continueVotes.length}/1)`);
webApp.SecondaryButton.setText(`Закончить (${stopVotes.length}/${users.length})`);
}, [votes])

const voteFinish = () => {
matchEvent.vote(card?.id ?? 0, 1);
voteEvent.vote(id ?? 0, 1);
};

const voteContinue = () => {
matchEvent.vote(card?.id ?? 0, 0);
voteEvent.vote(id ?? 0, 0);
};

// SecondaryButton слишком новая фича, либа ещё не имплементировала, надо будет сделать)
useEffect(() => {
webApp.MainButton.setText('Продолжить');
webApp.MainButton.show();
@@ -38,43 +52,67 @@ const MatchCard = () => {
};
}, [webApp]);

const [imageIndex, setImageIndex] = useState(0);

return (
<div
className="flex h-screen flex-col justify-center items-center overflow-hidden ${
className="flex h-screen pb-6 flex-col justify-center items-center overflow-hidden ${
isDragging"
>
<div className="text-3xl py-5">Это мэтч!</div>
<div
id="cardsWrapper"
className="w-full aspect-[30/35] max-w-[90vw] relative z-10"
>
<div className="relative h-full rounded-3xl overflow-hidden">
<div className="h-[380px] w-full">
<div className="bg-slate-100 h-full w-full rounded-3xl pb-4 overflow-hidden">
<img
draggable="false"
className="h-full w-auto min-w-full object-cover"
src={card?.images[0]}
/>
</div>
</div>
<div className="absolute top-0 w-full h-full">
<div className="absolute mx-auto bottom-2 text-xs">Все в лобби должны придти к единому решению!</div>
<div className="w-full z-50 absolute opacity-50 px-5 gap-4 flex p-2 top-0 justify-between">
{card !== null && card.images.length > 1 && card.images.map((_, index) => {
return (
<div
className="absolute pt-4 bottom-0 w-full rounded-3xl bg-secondary shadow-md overflow-hidden"
>
<h1 className="text-foreground text-lg font-medium mx-4">{card?.title}</h1>
<div className="h-full">
<p onClick={() => {
const url = `https://yandex.ru/maps/?rtext=${card?.location.lat}%2C${card?.location.lon}`
openLink(url);
}} className="p-4 pt-0 cursor-pointer underline flex flex-col justify-between overflow-hidden text-foreground">
{card?.address}
</p>
</div>
key={`image_${index}`}
className={`${index == imageIndex ? 'bg-muted' : 'bg-muted-foreground'} h-[2px] w-full rounded-full`}
/>
)
})}
</div>
<motion.div
onTap={(e: MouseEvent) => {
const boundingBox = (e.target as HTMLElement).getBoundingClientRect();
const tapX = e.clientX - boundingBox.left;
const elementWidth = boundingBox.width;

if (card == null) return;

setImageIndex((prevIndex) => {
if ((tapX + elementWidth / 4) < elementWidth / 2) {
return (prevIndex - 1 + card.images.length) % card.images.length;
} else if ((tapX - elementWidth / 4) > elementWidth / 2) {
return (prevIndex + 1) % card.images.length;
}
return prevIndex;
});
}}
className="relative h-full rounded-3xl overflow-hidden">
<div className="h-[380px] w-full">
<div className="bg-slate-100 h-full w-full rounded-3xl pb-4 overflow-hidden">
<img
draggable="false"
className="h-full w-auto min-w-full object-cover"
src={card?.images[imageIndex]}
/>
</div>
</div>
<div className="absolute top-0 w-full h-full">
<div
className="absolute pt-4 bottom-0 w-full rounded-3xl bg-secondary shadow-md overflow-hidden"
>
<h1 className="text-foreground text-lg font-medium mx-4">{card?.title}</h1>
<div className="h-full">
<p onClick={() => {
const url = `https://yandex.ru/maps/?rtext=${card?.location.lat}%2C${card?.location.lon}`
openLink(url);
}} className="p-4 pt-0 cursor-pointer underline flex flex-col justify-between overflow-hidden text-foreground">
{card?.address}
</p>
</div>
</div>
</div >
</div>
</div>
</motion.div >
</div>
);
};
2 changes: 2 additions & 0 deletions src/pages/game.page.tsx
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import { swipesEvent } from '@/shared/events/app-events/swipes.event';
import { releaseMatchEvent } from '@/shared/events/app-events/release-match.event';
import { finishEvent } from '@/shared/events/app-events/finish.event';
import { errorEvent } from '@/shared/events/app-events/error.event';
import { voteEvent } from '@/shared/events/app-events/vote.event';

export const GamePage = () => {
const { setLobbyId, lobbyId } = useLobbyStore();
@@ -43,6 +44,7 @@ export const GamePage = () => {
const unsubscribes = [
subscribe('card', (data) => cardEvent.handle(data)),
subscribe('match', (data) => matchEvent.handle(data)),
subscribe('voted', (data) => voteEvent.handle(data)),
subscribe('userJoined', (data) => userEvents.userJoin(data)),
subscribe('userLeft', (data) => userEvents.userLeft(data)),
subscribe('settingsUpdate', (data) => settingsUpdateEvent.handle(data)),
6 changes: 0 additions & 6 deletions src/pages/lobby-preview.page.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { swipesEvent } from '@/shared/events/app-events/swipes.event';
import { Toaster } from 'react-hot-toast';

import { AddPersonIcon } from '@/assets/icons/add-person.icon';
import { BOT_USERNAME } from '@/shared/constants';
@@ -97,11 +96,6 @@ export const LobbyPreviewPage = () => {

return (
<Layout>
<Toaster
toastOptions={{
className: '!bg-secondary !text-foreground !rounded-xl !w-full',
}}
/>
<AnimatePresence mode="wait">
<motion.div
key="lobbySettings"
6 changes: 0 additions & 6 deletions src/pages/lobby-settings.page.tsx
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ import { settingsUpdateEvent } from '@/shared/events/app-events/settings.event';

import { Tags } from '@/modules/settings/tags';
import { Users } from '@/modules/settings/users';
import { Toaster } from 'react-hot-toast';

export const LobbySettingsPage = () => {
const { settings, setState, users } = useLobbyStore();
@@ -67,11 +66,6 @@ export const LobbySettingsPage = () => {

return (
<Layout>
<Toaster
toastOptions={{
className: '!bg-secondary !text-foreground !rounded-xl !w-full',
}}
/>
<AnimatePresence mode="wait">
<motion.div
key="lobbySettings"
7 changes: 3 additions & 4 deletions src/shared/events/app-events/match.event.ts
Original file line number Diff line number Diff line change
@@ -3,24 +3,23 @@ import { getLobbyStoreMethods } from '@/shared/stores/lobby.store';
import { getMatchStoreMethods } from '@/shared/stores/match.store';
import { Match } from '@/shared/types/match.interface';
import { Event } from '../event';
import { getVoteStoreMethods } from '@/shared/stores/vote.store';

class MatchEvent extends Event {
handle(data: Match) {
const { setMatchCard } = getMatchStoreMethods();
const { resetStore } = getVoteStoreMethods();
const { setIsLoading } = getLoadingStoreMethods();
const { setState } = getLobbyStoreMethods();

resetStore();
setState('match');
setMatchCard({
id: data.id,
card: data.card,
});
setIsLoading(false);
}

vote(id: number, option: number) {
this.emit('vote', { id, option });
}
}

export const matchEvent = new MatchEvent();
41 changes: 21 additions & 20 deletions src/shared/events/app-events/user.event.ts
Original file line number Diff line number Diff line change
@@ -4,30 +4,31 @@ import { getLobbyStoreMethods } from '@/shared/stores/lobby.store';
import { Event } from '../event';

class UserEvents extends Event {
userJoin(data: User) {
const { addUser } = getLobbyStoreMethods();
console.log(data);
toast.success(`Пользователь ${data.name} присоединился`);
addUser({ ...data });
}
userJoin(data: User) {
const { addUser, state } = getLobbyStoreMethods();
if (state !== 'finished' && state !== 'error')
toast.success(`Пользователь ${data.name} присоединился`);
addUser({ ...data });
}

userLeft(data: User) {
const { removeUser } = getLobbyStoreMethods();
toast.error(`Пользователь ${data.name} вышел`);
removeUser(data.id);
}
userLeft(data: User) {
const { removeUser, state } = getLobbyStoreMethods();
if (state !== 'finished' && state !== 'error')
toast.error(`Пользователь ${data.name} вышел`);
removeUser(data.id);
}

joinLobby(lobbyId: string, userId: string | undefined) {
if (userId) {
this.emit('joinLobby', { userId, lobbyId });
} else {
console.error('user is not authenticated');
joinLobby(lobbyId: string, userId: string | undefined) {
if (userId) {
this.emit('joinLobby', { userId, lobbyId });
} else {
console.error('user is not authenticated');
}
}
}

handle(data: any): void {
console.log(data);
}
handle(data: any): void {
console.log(data);
}
}

export const userEvents = new UserEvents();
16 changes: 16 additions & 0 deletions src/shared/events/app-events/vote.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Event } from '../event';
import { Vote } from '@/shared/types/vote.interface';
import { getVoteStoreMethods } from '@/shared/stores/vote.store';

class VoteEvent extends Event {
handle(data: Vote) {
const { addVote } = getVoteStoreMethods();
addVote(data);
}

vote(id: number, option: number) {
this.emit('vote', { id, option });
}
}

export const voteEvent = new VoteEvent();
4 changes: 2 additions & 2 deletions src/shared/stores/lobby.store.ts
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ export const useLobbyStore = create<LobbyProps & LobbyActions>((set) => ({
}));

export function getLobbyStoreMethods() {
const { setCards, cards, addUser, removeUser, setSettings, setState } =
const { setCards, users, cards, addUser, removeUser, setSettings, setState, state } =
useLobbyStore.getState();
return { setCards, cards, addUser, removeUser, setSettings, setState };
return { setCards, users, cards, addUser, removeUser, setSettings, setState, state };
}
35 changes: 35 additions & 0 deletions src/shared/stores/vote.store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

import { create } from 'zustand';
import { Vote } from '../types/vote.interface';


type VoteActions = {
addVote: (newVote: Vote) => void;
resetStore: () => void;
}

type VoteProps = {
votes: Vote[];
};


export const useVoteStore = create<VoteActions & VoteProps>((set) => ({
votes: [],
addVote: (newVote: Vote) => {
console.log(newVote);
set((state) => ({
votes: [...state.votes.filter(x => x.User.id !== newVote.User.id), newVote],
}));
},
resetStore: () => {
set(() => ({
votes: [],
}));
},
}));

export function getVoteStoreMethods() {
const { votes, addVote, resetStore } =
useVoteStore.getState();
return { votes, addVote, resetStore };
}
7 changes: 7 additions & 0 deletions src/shared/types/vote.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { User } from "./user.interface";

export interface Vote {
id: number;
option: number;
User: User;
}

0 comments on commit f74a5af

Please sign in to comment.