diff --git a/bun.lockb b/bun.lockb index f8d6873..bb69d7a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a426593..b84eb3a 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,17 @@ "@icons-pack/react-simple-icons": "^9.5.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", "@tailwindcss/typography": "^0.5.13", "@tanstack/react-router": "^1.32.0", "@tanstack/react-store": "^0.4.1", + "@tanstack/react-table": "^8.17.3", + "@tanstack/react-virtual": "^3.5.0", "@tanstack/router-devtools": "^1.32.0", "@tanstack/router-vite-plugin": "^1.31.18", "@trivago/prettier-plugin-sort-imports": "^4.3.0", @@ -57,6 +61,7 @@ "react-hotkeys-hook": "^4.5.0", "react-resizable-panels": "^2.0.19", "react-use-precision-timer": "^3.5.5", + "react-virtuoso": "^4.7.10", "sonner": "^1.4.41", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.3", diff --git a/src/components/timer/drawScramble.tsx b/src/components/timer/drawScramble.tsx index d2c117a..32dead0 100644 --- a/src/components/timer/drawScramble.tsx +++ b/src/components/timer/drawScramble.tsx @@ -1,14 +1,11 @@ -import { useStore } from '@tanstack/react-store'; import { TwistyPlayer } from 'cubing/twisty'; import { useEffect, useRef, useState } from 'react'; import { cn } from '@/lib/utils'; -import { TimerStore } from './timerStore'; import cubeImage from '/cube-colors.png'; -export default function DrawScramble({ className }: { className: string }) { +export default function DrawScramble({ className, scramble }: { className: string, scramble: string}) { const containerRef = useRef(null); const [player, setPlayer] = useState(null); - const scramble = useStore(TimerStore, state => state.originalScramble); useEffect(() => { if (!containerRef.current) return; diff --git a/src/components/timer/timeList.tsx b/src/components/timer/timeList.tsx new file mode 100644 index 0000000..f38ba65 --- /dev/null +++ b/src/components/timer/timeList.tsx @@ -0,0 +1,184 @@ +import { useVirtualizer } from '@tanstack/react-virtual'; +import { useLiveQuery } from 'dexie-react-hooks'; +import { useMemo, useRef, useState } from 'react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Solve, db } from '@/lib/db'; +import { cn } from '@/lib/utils'; +import { Button } from '../ui/button'; +import { Plus, Trash2, X } from 'lucide-react'; +import DrawScramble from './drawScramble'; +import { ScrollArea } from '../ui/scroll-area'; +import { Separator } from '../ui/separator'; + +function convertTimeToText(time: number) { + if (time == -1) return 'DNF'; + + const minutes = Math.floor(time / 60000); + const seconds = Math.floor((time % 60000) / 1000); + const hundredths = Math.floor((time % 1000) / 10); + + let res = minutes > 0 ? `${minutes}:` : ''; + res += `${seconds < 10 && minutes > 0 ? '0' : ''}${seconds}.`; + res += `${hundredths < 10 ? '0' : ''}${hundredths}`; + + return res; +} + +function SolveDialog({ solve, idx, close }: { solve: Solve, idx: number, close: (open: boolean) => void }) { + return ( + + + + Solve #{idx} + + {new Date(solve.timeStamp).toLocaleString()} + + +

{convertTimeToText(solve.time)}

+

{solve.scramble}

+
+ + +
+
    +
  • Algs in solve:
  • + + {solve.parsed.map((alg, i) => ( +
  • + {alg} +
  • + ))} +
    +
+ + + +
+
+ ); +} + +export default function TimeList({ className }: { className: string }) { + const parentRef = useRef(null); + + const data = useLiveQuery(() => db.solves.reverse().toArray()); + + const fakeFullData = useMemo( + () => Array.from({ length: 5 }, () => data ?? []).flat(), + [data] + ); + + const rowVirtualizer = useVirtualizer({ + count: fakeFullData.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 45, + }); + + const items = rowVirtualizer.getVirtualItems(); + + const classes = cn('no-scrollbar', className); + + const padAmountForIdx = fakeFullData.length.toString().length; + + const [selectedSolve, setSelectedSolve] = useState(null); + + return ( +
+
+
+ {rowVirtualizer.getVirtualItems().map(item => { + const solve = fakeFullData[item.index]; + const timeText = convertTimeToText(solve.time); + + const reverseIdx = fakeFullData.length - item.index - 1; + + let mo3: string | undefined; + if (reverseIdx < 2) mo3 = '-'; + else { + const prevSolves = fakeFullData.slice(reverseIdx - 2, reverseIdx); + const mean = + prevSolves.reduce((acc, cur) => acc + cur.time, 0) / 3; + mo3 = convertTimeToText(mean); + } + + return ( +
setSelectedSolve(item.index)} + > +
+
{reverseIdx.toString().padStart(padAmountForIdx, ' ')}.
+
+
+
{timeText.padStart(7, ' ')}
+
+
+
{mo3.padStart(7, ' ')}
+
+
+ + + +
+
+ ); + })} +
+
+ {selectedSolve !== null && ( + setSelectedSolve(open ? selectedSolve : null)} + /> + )} +
+ ); +} diff --git a/src/components/timer/timerStore.ts b/src/components/timer/timerStore.ts index ab0a82b..e322f15 100644 --- a/src/components/timer/timerStore.ts +++ b/src/components/timer/timerStore.ts @@ -10,6 +10,7 @@ import { import { useEffect, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useStopwatch } from 'react-use-precision-timer'; +import { Solve, db } from '@/lib/db'; import { CubeStore } from '@/lib/smartCube'; import { extractAlgs } from '@/lib/solutionParser'; @@ -155,6 +156,8 @@ export const useCubeTimer = () => { moveDetails.current.end = [...CubeStore.state.lastMoves]; else moveDetails.current.end = []; + const endTime = stopwatch.getElapsedRunningTime(); + // get the ones in end which aren't in start const diff = moveDetails.current.end.filter( move => !moveDetails.current.start.includes(move) @@ -179,6 +182,18 @@ export const useCubeTimer = () => { ); newScramble(); + + const solutionStr = solution.join(' '); + console.log(solutionStr); + const solve = { + time: endTime, + timeStamp: Date.now(), + scramble: TimerStore.state.originalScramble, + solution: solutionStr, + parsed: algs.map(([alg]) => alg), + } as Solve; + + await db.solves.add(solve); }; const updateStateFromSpaceBar = (up: boolean) => { diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..cf253cf --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..7dc01b9 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = 'horizontal', decorative = true, ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/src/index.css b/src/index.css index 91ca1da..4c918b3 100644 --- a/src/index.css +++ b/src/index.css @@ -77,4 +77,16 @@ body { @apply bg-background text-foreground; } +} + +@layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } } \ No newline at end of file diff --git a/src/lib/db.ts b/src/lib/db.ts index 031f021..295bef5 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,20 +1,23 @@ import Dexie, { Table } from 'dexie'; -export interface Friend { +export interface Solve { id?: number; - name: string; - age: number; + timeStamp: number; + time: number; + scramble: string; + solution: string; + parsed: string[]; } export class MySubClassedDexie extends Dexie { // 'friends' is added by dexie when declaring the stores() // We just tell the typing system this is the case - friends!: Table; + solves!: Table; constructor() { super('myDatabase'); this.version(1).stores({ - friends: '++id, name, age', // Primary key and indexed props + solves: '++id, timestamp', }); } } diff --git a/src/lib/solutionParser.ts b/src/lib/solutionParser.ts index e796907..5809c96 100644 --- a/src/lib/solutionParser.ts +++ b/src/lib/solutionParser.ts @@ -358,6 +358,8 @@ export async function extractAlgs( isTwist: boolean, ][] = []; + moveSet = moveSet.slice(); + let moves = ''; let count = 0; @@ -438,6 +440,13 @@ export async function extractAlgs( algorithm: simplify(fixedAlg.join(' ')).toString(), outerBracket: true, })[0]; + + foundComm = foundComm.replaceAll('u', 'Uw'); + foundComm = foundComm.replaceAll('f', 'Fw'); + foundComm = foundComm.replaceAll('r', 'Rw'); + foundComm = foundComm.replaceAll('b', 'Bw'); + foundComm = foundComm.replaceAll('l', 'Lw'); + foundComm = foundComm.replaceAll('d', 'Dw'); } if (!isAnyAlg) { diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 235f829..96e7e33 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -4,7 +4,7 @@ import { Copy } from 'lucide-react'; import { useHotkeysContext } from 'react-hotkeys-hook'; import { Timer, TimerRenderer } from 'react-use-precision-timer'; import BTCubeDisplay from '@/components/cubing/btCubeDisplay'; -import DrawScramble from '@/components/timer/drawScramble'; +import TimeList from '@/components/timer/timeList'; import { TimerStore, useCubeTimer } from '@/components/timer/timerStore'; import { Button } from '@/components/ui/button'; import { @@ -110,16 +110,16 @@ function Dashboard() { /> -
+
-
+
Results - +
diff --git a/src/routes/twisty.tsx b/src/routes/twisty.tsx index a7a1653..8984a50 100644 --- a/src/routes/twisty.tsx +++ b/src/routes/twisty.tsx @@ -1,10 +1,33 @@ import { createFileRoute } from '@tanstack/react-router'; +import { useLiveQuery } from 'dexie-react-hooks'; import BTCubeDisplay from '@/components/cubing/btCubeDisplay'; +import TimeList from '@/components/timer/timeList'; +import { db } from '@/lib/db'; export const Route = createFileRoute('/twisty')({ component: TwistyExample, }); +export function SolveList() { + const friends = useLiveQuery(() => db.solves.toArray()); + + return ( +
    + {friends?.map(friend => ( +
  • + ({friend.id}): {friend.time} - {friend.parsed} - {friend.solution} +
  • + ))} +
+ ); +} + function TwistyExample() { - return ; + return ( + <> + + + {/* */} + + ); }