Skip to content

Commit ecce01d

Browse files
committed
feat: Picking card logic
1 parent 855a3db commit ecce01d

File tree

8 files changed

+131
-42
lines changed

8 files changed

+131
-42
lines changed

apps/web/src/app/(protected)/room/[roomCode]/game.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export const Game = () => {
2222
*
2323
* - [ ] Leave room button
2424
* - [ ] Toggle sound button
25-
* - [ ] Toggle fullscreen button
2625
*
2726
*/}
2827
</div>

apps/web/src/components/game/cards/black-card.tsx

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,65 @@ import { CardFooter } from './card-footer';
66
import { CardHeader } from './card-header';
77

88
export function BlackCard() {
9-
const { currentBlackCard } = useGame();
10-
// Determine text size class based on character count
11-
const textSizeClass = useMemo(() => {
12-
const text = currentBlackCard?.text || '';
13-
const charCount = text.length;
9+
const { currentBlackCard, selectedWhiteCards, handleUnpickWhiteCard } =
10+
useGame();
11+
12+
const combinedTextLength = useMemo(() => {
13+
if (!currentBlackCard?.text) return 0;
14+
15+
const blackCardText = currentBlackCard.text;
16+
const whiteCardsText =
17+
selectedWhiteCards?.map(card => card.text).join('') || '';
1418

15-
// Text size thresholds based on character count
16-
if (charCount <= 74) return 'text-4xl'; // Up to 74 chars
17-
if (charCount <= 100) return 'text-3xl'; // Up to 100 chars
18-
if (charCount <= 150) return 'text-2xl'; // Up to 150 chars
19-
if (charCount <= 200) return 'text-xl'; // Up to 200 chars
20-
return 'text-lg'; // More than 200 chars
21-
}, [currentBlackCard?.text]);
19+
return blackCardText.length + whiteCardsText.length;
20+
}, [currentBlackCard?.text, selectedWhiteCards]);
21+
22+
const textSizeClass = useMemo(() => {
23+
if (combinedTextLength <= 50) return 'text-3xl';
24+
if (combinedTextLength <= 70) return 'text-2xl';
25+
if (combinedTextLength <= 100) return 'text-xl';
26+
if (combinedTextLength <= 150) return 'text-lg';
27+
return 'text-base';
28+
}, [combinedTextLength]);
2229

23-
// Process text to replace "___" with styled lines
2430
const processedContent = useMemo(() => {
2531
const text = currentBlackCard?.text || '';
26-
27-
// If no fill-in-the-gap markers, return the text as is
2832
if (!text.includes('___')) return text;
2933

30-
// Split by the fill-in-the-gap marker
3134
const segments = text.split('___');
35+
const whiteCards = selectedWhiteCards || [];
3236

33-
// Map through segments and join with styled lines
3437
return segments.map((segment, index) => {
35-
// Last segment doesn't need a line after it
3638
if (index === segments.length - 1) {
3739
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
38-
return <span key={index}>{segment}</span>;
40+
return <span key={`segment-${index}`}>{segment}</span>;
3941
}
4042

41-
// Add a line after each segment (except the last)
43+
const hasWhiteCard = index < whiteCards.length;
44+
4245
return (
4346
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
44-
<span key={index}>
47+
<span key={`segment-${index}`}>
4548
{segment}
46-
<span className="mx-1 inline-block h-4 w-24 border-b-2 border-zinc-50 align-middle" />
49+
{hasWhiteCard ? (
50+
<button
51+
type="button"
52+
onClick={() => handleUnpickWhiteCard(whiteCards[index])}
53+
className="my-1 inline-block rounded bg-white px-2 py-1 text-left font-bold text-zinc-950"
54+
>
55+
{whiteCards[index].text}
56+
</button>
57+
) : (
58+
// Display empty blank
59+
<span className="mx-1 inline-block h-4 w-24 border-b-2 border-white align-middle" />
60+
)}
4761
</span>
4862
);
4963
});
50-
}, [currentBlackCard?.text]);
64+
}, [currentBlackCard?.text, selectedWhiteCards, handleUnpickWhiteCard]);
5165

5266
return (
53-
<div className="flex aspect-card h-[30rem] flex-col justify-between gap-5 rounded-2xl border border-zinc-950 bg-zinc-950 p-9 dark:border-zinc-50">
67+
<div className="flex aspect-card h-96 flex-col justify-between gap-2 rounded-2xl border border-zinc-950 bg-zinc-950 p-6 dark:border-zinc-50">
5468
<CardHeader packId={currentBlackCard?.packId || ''} variant="blackCard" />
5569

5670
<div className="flex flex-1 items-center">

apps/web/src/components/game/cards/white-card.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
'use client';
2+
13
import type { WhiteCard as WhiteCardType } from '@caho/schemas';
24
import { cn } from '@/utils/cn';
5+
import { useGame } from '@/hooks/game';
36
import { CardFooter } from './card-footer';
47
import { CardHeader } from './card-header';
58

@@ -9,12 +12,18 @@ type WhiteCardProps = {
912
};
1013

1114
export function WhiteCard({ data, className }: WhiteCardProps) {
15+
const { handlePickWhiteCard, isWhiteCardPickingDisabled } = useGame();
1216
const { packId, text } = data;
1317

1418
return (
15-
<div
19+
<button
20+
type="button"
21+
disabled={isWhiteCardPickingDisabled}
22+
onClick={() => handlePickWhiteCard(data)}
1623
className={cn(
17-
'flex aspect-card h-72 flex-col justify-between gap-2 rounded-xl border border-zinc-950 bg-zinc-50 p-5 transition-all hover:scale-110 dark:border-zinc-50',
24+
'flex aspect-card h-72 flex-col justify-between gap-2 rounded-xl border-2 border-zinc-100 bg-white p-5 text-left shadow-xl transition-all disabled:cursor-not-allowed dark:border-zinc-50',
25+
!isWhiteCardPickingDisabled &&
26+
'hover:-translate-y-4 hover:scale-110 hover:cursor-pointer',
1827
className
1928
)}
2029
>
@@ -25,6 +34,6 @@ export function WhiteCard({ data, className }: WhiteCardProps) {
2534
</div>
2635

2736
<CardFooter variant="whiteCard" />
28-
</div>
37+
</button>
2938
);
3039
}
Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
'use client';
22

3+
import { cn } from '@/utils/cn';
34
import { useGame } from '@/hooks/game';
5+
import { Button } from '../ui/button';
46
import { WhiteCard } from './cards/white-card';
57

68
export function GamePlayerDeck() {
7-
// TODO: -space-x-10 group hover:space-x-8 stack arc */
8-
const { currentWhiteCards, currentPlayer } = useGame();
9-
9+
const { currentWhiteCards, currentPlayer, isWhiteCardPickingDisabled } =
10+
useGame();
1011
const isJudge = currentPlayer.isJudge;
1112

12-
console.log(currentPlayer);
13+
currentWhiteCards.sort((a, b) => a.text.localeCompare(b.text));
1314

1415
if (isJudge) {
1516
return (
@@ -20,10 +21,31 @@ export function GamePlayerDeck() {
2021
}
2122

2223
return (
23-
<div className="flex w-full items-center justify-center space-x-4">
24+
<div
25+
className={cn(
26+
'relative flex h-full max-h-96 w-full max-w-full snap-x snap-mandatory snap-always flex-row items-center justify-center -space-x-32 overflow-x-auto scroll-smooth hover:overflow-x-scroll'
27+
)}
28+
>
2429
{currentWhiteCards.map(card => (
2530
<WhiteCard key={card.id} data={card} />
2631
))}
32+
33+
<div
34+
className={cn(
35+
'absolute inset-0 z-10 flex flex-col items-center justify-center gap-8 overflow-clip backdrop-blur-sm before:absolute before:bottom-0 before:-z-10 before:aspect-square before:h-1/2 before:translate-y-1/2 before:rounded-full before:bg-zinc-950 before:blur-[120px] after:absolute after:inset-0 after:-z-20 after:bg-gradient-to-b after:from-white after:to-white/10 after:content-[""] before:dark:bg-zinc-400 after:dark:from-zinc-950 after:dark:to-zinc-950/90',
36+
!isWhiteCardPickingDisabled && 'hidden'
37+
)}
38+
>
39+
<header className="flex max-w-xs flex-col items-center justify-center gap-1 text-center ">
40+
<h3 className="text-2xl font-bold">Escolheu sua atrocidade?</h3>
41+
<span className="text-balance text-sm text-zinc-700 dark:text-zinc-300">
42+
Para desfazer uma carta jogada, basta clicar no texto dela na carta
43+
preta e ela retornará ao baralho
44+
</span>
45+
</header>
46+
47+
<Button size="lg">Estou pronto!</Button>
48+
</div>
2749
</div>
2850
);
2951
}

apps/web/src/components/game/ongoing/ongoing.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ export const Ongoing = () => {
66
return (
77
<main className="flex h-[calc(100vh-4rem)] w-full flex-1">
88
<GameRankingSidebar />
9-
<div className="flex flex-1 flex-col items-center justify-between p-8">
10-
<BlackCard />
9+
<div className="flex flex-1 flex-col">
10+
<div className="flex flex-1 items-center justify-center p-8">
11+
<BlackCard />
12+
</div>
1113

1214
<GamePlayerDeck />
1315
</div>

apps/web/src/components/game/ranking/game-ranking-sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function GameRankingSidebar() {
2929
<ScrollArea
3030
className={cn(
3131
'h-[calc(100vh-4rem)] flex-col items-center justify-between border-r border-zinc-200 p-4 dark:border-zinc-900',
32-
isCollapsed ? 'w-28' : 'relative max-w-sm flex-1'
32+
isCollapsed ? 'w-28' : 'relative w-96 shrink-0'
3333
)}
3434
>
3535
<GameRankingHeader />

apps/web/src/hooks/game.tsx

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import type { PlayerEvent, RoomEvent } from '@caho/contracts';
55
import type { BlackCard, Player, Room, WhiteCard } from '@caho/schemas';
66
import { env } from '@/env.mjs';
77

8-
// 30 seconds
9-
const PING_INTERVAL = 30000;
8+
const PING_INTERVAL_IN_SECONDS = 30 * 1000;
109

1110
type GameContextType = {
12-
currentPlayer: Player;
13-
players: Player[];
1411
room: Room;
12+
currentPlayer: Player;
1513
currentBlackCard: BlackCard | null;
1614
currentWhiteCards: WhiteCard[];
15+
selectedWhiteCards: WhiteCard[];
16+
handlePickWhiteCard: (card: WhiteCard) => void;
17+
handleUnpickWhiteCard: (card: WhiteCard) => void;
18+
isWhiteCardPickingDisabled: boolean;
19+
players: Player[];
1720
};
1821

1922
type WebsocketEvent = PlayerEvent | RoomEvent;
@@ -47,6 +50,43 @@ export const GameContextProvider = ({
4750
const [currentWhiteCards, setCurrentWhiteCards] = useState<WhiteCard[]>(
4851
initialWhiteCards ?? []
4952
);
53+
const [selectedWhiteCards, setSelectedWhiteCards] = useState<WhiteCard[]>([]);
54+
55+
const cardsToPickAmount = currentBlackCard?.pick ?? 0;
56+
const isWhiteCardPickingDisabled =
57+
selectedWhiteCards?.length === cardsToPickAmount;
58+
59+
const handlePickWhiteCard = (card: WhiteCard) => {
60+
const isAlreadySelected = selectedWhiteCards.some(
61+
existingCard => existingCard.id === card.id
62+
);
63+
64+
if (isAlreadySelected || isWhiteCardPickingDisabled) {
65+
return;
66+
}
67+
68+
setSelectedWhiteCards(prev => {
69+
if (isAlreadySelected) {
70+
return prev.filter(prevCard => prevCard.id !== card.id);
71+
}
72+
73+
return [...prev, card];
74+
});
75+
76+
setCurrentWhiteCards(prev => {
77+
return prev.filter(prevCard => prevCard.id !== card.id);
78+
});
79+
};
80+
81+
const handleUnpickWhiteCard = (card: WhiteCard) => {
82+
setSelectedWhiteCards(prev =>
83+
prev.filter(prevCard => prevCard.id !== card.id)
84+
);
85+
86+
setCurrentWhiteCards(prev => {
87+
return [...prev, card];
88+
});
89+
};
5090

5191
useEffect(() => {
5292
const roomWs = new WebSocket(
@@ -125,7 +165,7 @@ export const GameContextProvider = ({
125165
ws.send('ping');
126166
console.log('[WS]: Ping sent');
127167
}
128-
}, PING_INTERVAL);
168+
}, PING_INTERVAL_IN_SECONDS);
129169

130170
ws.onclose = () => {
131171
console.log(`[WS]: Disconnected from the WebSocket server ${ws.url}`);
@@ -157,6 +197,10 @@ export const GameContextProvider = ({
157197
currentPlayer,
158198
currentBlackCard,
159199
currentWhiteCards,
200+
handlePickWhiteCard,
201+
handleUnpickWhiteCard,
202+
isWhiteCardPickingDisabled,
203+
selectedWhiteCards,
160204
players
161205
}}
162206
>

apps/web/src/styles/globals.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,3 @@
6565

6666
/*TODO: remove ugly input type number arrow button at the right */
6767
}
68-

0 commit comments

Comments
 (0)