A daily game

Display hints when right or close

+88 -49
+27 -17
src/components/game/variant-header.tsx
··· 1 - import { useBoardSubset } from "@/lib/hooks"; 1 + import { groupByValue } from "@/lib/game"; 2 + import { useBoardSubset, useSessionSubset } from "@/lib/hooks"; 2 3 import { cva } from "class-variance-authority"; 4 + import { useMemo } from "react"; 3 5 4 - const variantHeader = cva("flex gap-2 text-sm text-main font-bold", { 6 + const variantHeader = cva("flex gap-2 text-sm font-bold", { 5 7 variants: { 6 8 orientation: { 7 9 horizontal: "", 8 10 vertical: "flex-col items-center", 11 + }, 12 + result: { 13 + correct: "text-lime-400", 14 + almost: "text-amber-300", 15 + inProgress: "text-main", 9 16 }, 10 17 }, 11 18 }); 12 19 13 20 type Props = { 21 + orientation: "horizontal" | "vertical"; 22 + index: number; 14 23 variant: number; 15 24 }; 16 25 17 - type RowProps = { 18 - row: number; 19 - } & Props; 26 + export function VariantHeader({ orientation, index, variant }: Props) { 27 + const dir = orientation === "horizontal" ? "row" : "col"; 28 + const boardRow = useBoardSubset(dir, index); 29 + const boardHints = groupByValue(boardRow, variant); 30 + const sessionRow = useSessionSubset(dir, index); 31 + const sessionHints = groupByValue(sessionRow, variant); 32 + // TODO This re-renders way too much because of the selectAtom 20 33 21 - type ColProps = { 22 - col: number; 23 - } & Props; 34 + const result = 35 + sessionRow.toString() === boardRow.toString() 36 + ? "correct" 37 + : sessionHints.toString() === boardHints.toString() && 38 + sessionRow.filter((i) => i > -1).length === boardRow.length 39 + ? "almost" 40 + : "inProgress"; 24 41 25 - export function VariantHeader({ variant, ...props }: RowProps | ColProps) { 26 - const orientation = "row" in props ? "horizontal" : "vertical"; 27 - const block = useBoardSubset( 28 - "row" in props ? "row" : "col", 29 - "row" in props ? props.row : props.col, 30 - variant, 31 - ); 32 42 return ( 33 - <div className={variantHeader({ orientation })}> 34 - {block.map((count, i) => ( 43 + <div className={variantHeader({ orientation, result })}> 44 + {boardHints.map((count, i) => ( 35 45 <p key={i}>{count}</p> 36 46 ))} 37 47 </div>
+8 -12
src/components/game/variant-hints.tsx
··· 22 22 blocks: number[][]; 23 23 } 24 24 25 - function headerProps<T extends "row" | "col">(orientation: T) { 26 - return (index: number) => { 27 - return { 28 - [orientation]: index, 29 - } as Record<T, number>; 30 - }; 31 - } 32 - 33 25 export function VariantHints({ variant, position, blocks }: Props) { 34 26 const orientation = ["top", "bottom"].includes(position) 35 27 ? "horizontal" 36 28 : "vertical"; 37 - const propsFactory = headerProps( 38 - orientation === "horizontal" ? "col" : "row", 39 - ); 40 29 return ( 41 30 <div className={header({ position, orientation })}> 42 31 {blocks.map((_, i) => ( ··· 49 38 position === "top" ? "justify-end" : "", 50 39 ].join(" ")} 51 40 > 52 - <VariantHeader key={i} variant={variant} {...propsFactory(i)} /> 41 + <VariantHeader 42 + key={i} 43 + orientation={ 44 + orientation === "horizontal" ? "vertical" : "horizontal" 45 + } 46 + index={i} 47 + variant={variant} 48 + /> 53 49 </div> 54 50 ))} 55 51 </div>
+13 -18
src/lib/hooks.ts
··· 1 1 import { useAtom, useAtomValue } from "jotai"; 2 2 import { useMemo } from "react"; 3 3 import { 4 - _boardAtom, 5 4 boardAtom, 6 5 boardMatrixAtom, 7 6 gameResultAtom, 8 - getGuessAtomAt, 9 - getVariantAtomAt, 10 7 heightAtom, 11 8 preferencesAtom, 9 + selectBoardSubset, 10 + selectGuessAt, 11 + selectSessionSubset, 12 + selectVariantAt, 12 13 sessionMatrixAtom, 13 14 variantsAtom, 14 15 widthAtom, 15 16 } from "./state"; 16 - import { groupByValue, groupIntoCols, groupIntoRows } from "./game"; 17 17 18 18 export function useGuessAt(index: number) { 19 - const atomCell = useMemo(() => getGuessAtomAt(index), [index]); 19 + const atomCell = useMemo(() => selectGuessAt(index), [index]); 20 20 const [cell] = useAtom(atomCell); 21 21 return cell; 22 22 } ··· 26 26 } 27 27 28 28 export function useVariantAt(index: number) { 29 - const variantAtom = useMemo(() => getVariantAtomAt(index), [index]); 29 + const variantAtom = useMemo(() => selectVariantAt(index), [index]); 30 30 return useAtomValue(variantAtom); 31 31 } 32 32 ··· 52 52 return useAtomValue(gameResultAtom); 53 53 } 54 54 55 - export function useBoardSubset( 56 - orientation: "row" | "col", 57 - index: number, 58 - variant: number, 59 - ) { 60 - const [width] = useBoardDimensions(); 61 - const board = useBoard(); 55 + export function useBoardSubset(dir: "row" | "col", index: number) { 56 + const subset = useMemo(() => selectBoardSubset(dir, index), [dir, index]); 57 + return useAtomValue(subset); 58 + } 62 59 63 - if (orientation === "row") { 64 - return groupByValue(groupIntoRows(board, width)[index]!, variant); 65 - } else { 66 - return groupByValue(groupIntoCols(board, width)[index]!, variant); 67 - } 60 + export function useSessionSubset(dir: "row" | "col", index: number) { 61 + const subset = useMemo(() => selectSessionSubset(dir, index), [dir, index]); 62 + return useAtomValue(subset); 68 63 } 69 64 70 65 export function usePreferences() {
+40 -2
src/lib/state.ts
··· 103 103 }, 104 104 ); 105 105 106 - export const getGuessAtomAt = (index: number) => 106 + export const selectBoardSubset = (dir: "row" | "col", index: number) => 107 + selectAtom(boardMatrixAtom, (board) => { 108 + if (dir === "row") { 109 + const row = board[index]; 110 + if (typeof row === "undefined") { 111 + throw new Error(`No board ${dir} at index ${index}`); 112 + } 113 + return row; 114 + } else { 115 + return board.map((row) => { 116 + const col = row[index]; 117 + if (typeof col === "undefined") { 118 + throw new Error(`No board ${dir} at index ${index}`); 119 + } 120 + return col; 121 + }); 122 + } 123 + }); 124 + 125 + export const selectSessionSubset = (dir: "row" | "col", index: number) => 126 + selectAtom(sessionMatrixAtom, (session) => { 127 + if (dir === "row") { 128 + const row = session[index]; 129 + if (typeof row === "undefined") { 130 + throw new Error(`No session ${dir} at index ${index}`); 131 + } 132 + return row; 133 + } else { 134 + return session.map((row) => { 135 + const col = row[index]; 136 + if (typeof col === "undefined") { 137 + throw new Error(`No session ${dir} at index ${index}`); 138 + } 139 + return col; 140 + }); 141 + } 142 + }); 143 + 144 + export const selectGuessAt = (index: number) => 107 145 selectAtom(sessionAtom, (cells) => { 108 146 const cellAtom = cells[index]; 109 147 if (typeof cellAtom === "undefined") { ··· 112 150 return cellAtom; 113 151 }); 114 152 115 - export function getVariantAtomAt(index: number) { 153 + export function selectVariantAt(index: number) { 116 154 return selectAtom(variantsAtom, (variants) => { 117 155 const variant = variants[index]; 118 156 if (typeof variant === "undefined") {