Skip to content

Commit

Permalink
feat: enchase board dock
Browse files Browse the repository at this point in the history
  • Loading branch information
ItaloMedici committed Feb 15, 2025
1 parent 8f5c126 commit 9c9ca3a
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 125 deletions.
215 changes: 133 additions & 82 deletions app/(dashboard)/room/[roomId]/_components/board-dock.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,155 @@
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useBoard } from "@/context/board";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { Eraser, EyeIcon, EyeOffIcon, RotateCcw } from "lucide-react";

const ICON_SIZE = 16;
import { UNLIMITED_PLAN_VALUE } from "@/lib/consts";
import { cn } from "@/lib/utils";
import { fibonacciAverageOptions } from "@/use-cases/board/choice-options";
import { ArrowRight, Eye, Loader, Plus, Users } from "lucide-react";
import { useMemo } from "react";

export const BoardDock = () => {
const {
others,
handleRevealCards,
reveal,
handleReset,
average,
totalChoices,
totalPlayers,
availableRounds,
closestStoryPoint,
handlePlay,
loadingPlay,
agreementPercentage,
} = useBoard();
const isMobile = useIsMobile();

if (!others.length) {
return null;
}

const onResetClick = () => {
if (reveal) {
handleRevealCards();
}

handleReset();
};

const agreementIcon = () => {
if (agreementPercentage === 95) {
return "🤩";
}

if (agreementPercentage >= 75) {
return "😍";
}
const averageEmoji = useMemo(() => {
if (!reveal || !average) return "🃏";

if (agreementPercentage >= 50) {
return "🤔";
}
if (average <= fibonacciAverageOptions.small) return "☕";
if (average <= fibonacciAverageOptions.medium) return "👍";
if (average <= fibonacciAverageOptions.large) return "🤔";

if (agreementPercentage >= 25) {
return "😐";
}
return "💀";
}, [average, reveal]);

return "🤨";
const formatAverage = (value: number | null | undefined) => {
if (value === null || value === undefined) return "-";
return Number(value).toFixed(1).replace(/\.0$/, "");
};

const ResultDesktop = () => {
if (!reveal || isMobile) return null;
const votingProcess = useMemo(() => {
return `${totalChoices}/${totalPlayers} votos`;
}, [totalChoices, totalPlayers]);

const playAction = useMemo(() => {
if (loadingPlay)
return (
<>
<Loader className="h-4 w-4 animate-spin" />
<span>Carregando...</span>
</>
);

if (reveal)
return (
<>
<Plus className="h-4 w-4" />
<span>Nova Rodada</span>
</>
);

return (
<div className="flex items-center gap-2 bg-white p-1 rounded-md border border-gray-200 font-medium">
<span className="text-md mr-2">{agreementIcon()}</span>
<span className="text-xs text-gray-500">Média:</span>
<span className="text-sm text-gray-800">{average}</span>
<span className="text-xs text-gray-500">Concordância:</span>
<span className="text-sm text-gray-800">{agreementPercentage}%</span>
</div>
<>
<Eye className="h-4 w-4" />
<span>Revelar</span>
</>
);
};

const ResultMobile = () => {
if (!reveal || !isMobile) return null;

return (
<div className="flex items-center w-fit gap-2 bg-white p-2 rounded-lg border border-gray-200">
<span className="text-md mr-2">{agreementIcon()}</span>
<span className="text-sm text-gray-500">Média:</span>
<span className="text-sm text-gray-800">{average}</span>
<span className="text-sm text-gray-500">Concordância:</span>
<span className="text-sm text-gray-800">{agreementPercentage}%</span>
</div>
);
};
}, [reveal, loadingPlay]);

return (
<div className="flex flex-col gap-4 items-center">
<div className="border bg-gray-50 border-gray-200 p-[6px] rounded-lg flex items-center justify-between gap-[6px]">
<Button onClick={handleRevealCards} size={"sm"} variant={"outline"}>
{reveal ? (
<EyeOffIcon size={ICON_SIZE} />
) : (
<EyeIcon size={ICON_SIZE} />
)}{" "}
{reveal ? "Esconder" : "Revelar"}
</Button>

<Button onClick={onResetClick} size={"sm"} variant={"outline"}>
<RotateCcw size={ICON_SIZE} /> Reiniciar
</Button>

<Button variant={"ghost"} onClick={handleReset} size={"sm"}>
<Eraser size={ICON_SIZE} /> Limpar
</Button>

<ResultDesktop />
</div>
<ResultMobile />
</div>
<TooltipProvider>
<Card className="flex flex-col items-stretch gap-2 p-2 shadow-lg transition-all duration-200 hover:shadow-md">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button size="sm" onClick={handlePlay} disabled={loadingPlay}>
{playAction}
</Button>
</div>

<Tooltip>
<TooltipTrigger asChild>
<div className="ml-6 flex items-center gap-4 rounded-md">
<div className="flex items-center gap-4">
<span className="text-3xl">{averageEmoji}</span>
<div className="flex flex-col">
<span className="text-xs text-muted-foreground opacity-95">
Média
</span>
<span className="text-xs">
{average === 0 || !reveal ? (
"-"
) : (
<div className="flex items-center gap-1">
<span>{formatAverage(average)}</span>
<ArrowRight className="h-3 w-3 opacity-70" />
<span>{closestStoryPoint}</span>
</div>
)}
</span>
</div>
</div>
<Separator orientation="vertical" className="h-8" />
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{votingProcess}
</span>
</div>
</div>
</TooltipTrigger>
<TooltipContent>
<div className="flex flex-col gap-4 text-sm">
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Média atual:</span>
<span className="font-semibold">
{formatAverage(average)}
</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">
Pontuação sugerida:
</span>
<span className="font-semibold">{closestStoryPoint}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Concordância:</span>
<span className="font-semibold">{agreementPercentage}%</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">
Rodadas restantes:
</span>
<span
className={cn("font-semibold", {
"text-red-500": availableRounds === 0,
"text-orange-500": availableRounds <= 1,
})}
>
{availableRounds === UNLIMITED_PLAN_VALUE
? "Ilimitadas"
: availableRounds}
</span>
</div>
</div>
</TooltipContent>
</Tooltip>
</div>
</Card>
</TooltipProvider>
);
};
86 changes: 55 additions & 31 deletions context/board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ type BoardContextProps = {
totalChoices: number;
average: number;
agreementPercentage: number;
availableRounds: number;
closestStoryPoint: number;
handleChoice: (choice: string) => Promise<void>;
handleRevealCards: () => Promise<void>;
handleReset: () => Promise<void>;
handlePlay: () => Promise<void>;
loadingPlay: boolean;
handleNotifyPlayer: (
playerId: string,
notification?: EnumNotification,
Expand All @@ -73,7 +75,9 @@ export const BoardProvider = ({
const [boardStatus, setBoardStatus] = useState<BoardStatus>(
{} as BoardStatus,
);
const [isLoading, setIsLoading] = useState(true);
const [isOnFirstLoad, setIsOnFirstLoad] = useState(true);
const [loadingPlay, setPlayLoading] = useState(false);

const [selfChoice, setSelfChoice] = useState("");
const [revealOptimistc, setRevealOptimistc] = useState(
boardStatus?.reveal || false,
Expand Down Expand Up @@ -101,30 +105,27 @@ export const BoardProvider = ({
return board;
}, [roomId, router]);

const onEventSourceMessage = useCallback(
(event: MessageEvent) => {
const data = JSON.parse(event.data) as BoardStatus;
const onEventSourceMessage = useCallback((event: MessageEvent) => {
const data = JSON.parse(event.data) as BoardStatus;

if (isLoading) setIsLoading(false);
if (isOnFirstLoad) setIsOnFirstLoad(false);

if (boardSnapshot.current === JSON.stringify(data)) return;
if (boardSnapshot.current === JSON.stringify(data)) return;

boardSnapshot.current = JSON.stringify(data);
boardSnapshot.current = JSON.stringify(data);

setBoardStatus(data);
setBoardStatus(data);

if (!selfChoiceRef.current && typeof data?.self?.choice === "string") {
setSelfChoice(data.self.choice);
}
if (!selfChoiceRef.current && typeof data?.self?.choice === "string") {
setSelfChoice(data.self.choice);
}

if (typeof data?.reveal === "boolean") {
setRevealOptimistc(data.reveal);
}
if (typeof data?.reveal === "boolean") {
setRevealOptimistc(data.reveal);
}

selfId.current = data?.self?.id;
},
[isLoading],
);
selfId.current = data?.self?.id;
}, []);

const subscribeToBoardStatus = useCallback(async () => {
if (hasSubscribed.current) return;
Expand Down Expand Up @@ -158,7 +159,7 @@ export const BoardProvider = ({
return () => {
unsubscribeToBoardStatus();
};
}, [subscribeToBoardStatus, unsubscribeToBoardStatus]);
}, []);

useEffect(() => {
if (!boardStatus?.boardId) return;
Expand Down Expand Up @@ -286,15 +287,9 @@ export const BoardProvider = ({
);

if (updatedStatus) setBoardStatus(updatedStatus);
}, [boardStatus.self?.id, roomId]);
}, [boardStatus.availableRounds, boardStatus.self?.id, roomId]);

const handleReset = useCallback(async () => {
if (boardStatus.availableRounds === 0) {
toast.error("Você atingiu o limite de rodadas.");
setOpenPlanOfferDialog(true);
return;
}

setSelfChoice("");
setBoardStatus((prev) => {
return {
Expand All @@ -321,6 +316,33 @@ export const BoardProvider = ({
roomId,
]);

const handlePlay = useCallback(async () => {
try {
setPlayLoading(true);

if (boardStatus.availableRounds === 0) {
toast.error("Você atingiu o limite de rodadas.");
setOpenPlanOfferDialog(true);
return;
}

if (revealOptimistc) {
setRevealOptimistc(false);
await handleReset();
return;
}

await handleRevealCards();
} finally {
setPlayLoading(false);
}
}, [
handleRevealCards,
handleReset,
boardStatus.availableRounds,
revealOptimistc,
]);

const handleNotifyPlayer = useCallback(
async (playerId: string, notification?: EnumNotification) => {
const updatedStatus = await http.post<BoardStatus>(
Expand All @@ -345,8 +367,8 @@ export const BoardProvider = ({
reveal: revealOptimistc,
selfChoice,
handleChoice,
handleRevealCards,
handleReset,
handlePlay,
loadingPlay,
handleNotifyPlayer,
handleLeave: leaveBoard,
}),
Expand All @@ -360,10 +382,12 @@ export const BoardProvider = ({
handleReset,
handleNotifyPlayer,
leaveBoard,
loadingPlay,
handlePlay,
],
);

if (!boardStatus?.self || isLoading) {
if (!boardStatus?.self || isOnFirstLoad) {
return <LoadingLogo />;
}

Expand Down
Loading

0 comments on commit 9c9ca3a

Please sign in to comment.