Skip to content

Commit

Permalink
Add table for results
Browse files Browse the repository at this point in the history
  • Loading branch information
simonkellly committed May 22, 2024
1 parent 99dce62 commit a6011d9
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 14 deletions.
Binary file modified bun.lockb
Binary file not shown.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions src/components/timer/drawScramble.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const [player, setPlayer] = useState<TwistyPlayer | null>(null);
const scramble = useStore(TimerStore, state => state.originalScramble);

useEffect(() => {
if (!containerRef.current) return;
Expand Down
184 changes: 184 additions & 0 deletions src/components/timer/timeList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={true} onOpenChange={close}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Solve #{idx}</DialogTitle>
<DialogDescription>
{new Date(solve.timeStamp).toLocaleString()}
</DialogDescription>
</DialogHeader>
<h2 className='text-3xl font-bold'>{convertTimeToText(solve.time)}</h2>
<h2 className='text-l font-bold'>{solve.scramble}</h2>
<div className="flex">
<DrawScramble scramble={solve.scramble} className='w-full h-32 mx-auto' />
<DrawScramble scramble={solve.scramble + ' ' + solve.solution} className='w-full h-32 mx-auto' />
</div>
<ul className='rounded-md border p-2'>
<li className="font-medium">Algs in solve:</li>
<ScrollArea className='h-64'>
{solve.parsed.map((alg, i) => (
<li key={i + " " + alg}>
{alg}
</li>
))}
</ScrollArea>
</ul>
<DialogFooter>
<Button variant="destructive" type="submit">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

export default function TimeList({ className }: { className: string }) {
const parentRef = useRef<HTMLDivElement | null>(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<number | null>(null);

return (
<div
ref={parentRef}
className={classes}
style={{
overflowY: 'auto',
contain: 'strict',
}}
>
<div
style={{
height: rowVirtualizer.getTotalSize(),
width: '100%',
position: 'relative',
}}
>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${items[0]?.start ?? 0}px)`,
}}
>
{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 (
<div
key={item.key}
className="flex gap-2 rounded-md font-mono py-1 pl-2 my-1 hover:bg-gray-900 cursor-pointer"
ref={rowVirtualizer.measureElement}
data-index={item.index}
onClick={() => setSelectedSolve(item.index)}
>
<div className="text-gray-500 my-auto">
<pre>{reverseIdx.toString().padStart(padAmountForIdx, ' ')}.</pre>
</div>
<div className="text-right px-1 my-auto">
<pre>{timeText.padStart(7, ' ')}</pre>
</div>
<div className="text-right px-1 my-auto">
<pre>{mo3.padStart(7, ' ')}</pre>
</div>
<div className="text-right px-1 grow">
<Button
variant="ghost"
size="sm"
>
<X className="size-4" />
</Button>
<Button
variant="ghost"
size="sm"
>
<Plus className="size-4" />
</Button>
<Button
variant="ghost"
size="sm"
>
<Trash2 className="size-4" />
</Button>
</div>
</div>
);
})}
</div>
</div>
{selectedSolve !== null && (
<SolveDialog
solve={fakeFullData[selectedSolve]}
idx={fakeFullData.length - 1 - selectedSolve}
close={(open) => setSelectedSolve(open ? selectedSolve : null)}
/>
)}
</div>
);
}
15 changes: 15 additions & 0 deletions src/components/timer/timerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)
Expand All @@ -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) => {
Expand Down
46 changes: 46 additions & 0 deletions src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }
28 changes: 28 additions & 0 deletions src/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = 'horizontal', decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;

export { Separator };
12 changes: 12 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
}
13 changes: 8 additions & 5 deletions src/lib/db.ts
Original file line number Diff line number Diff line change
@@ -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<Friend>;
solves!: Table<Solve>;

constructor() {
super('myDatabase');
this.version(1).stores({
friends: '++id, name, age', // Primary key and indexed props
solves: '++id, timestamp',
});
}
}
Expand Down
Loading

0 comments on commit a6011d9

Please sign in to comment.