From 26b6c63caf3be1b2b50ae35dd2d81060c8cbd7a7 Mon Sep 17 00:00:00 2001 From: Mihkel Martin Kasterpalu <qpeuitovxg@use.startmail.com> Date: Tue, 21 Jan 2025 11:42:52 +0200 Subject: [PATCH] Better styling and loading logic, save state for user session --- src/lib/components/DNDGroup.svelte | 58 +++++--- .../alert-dialog/alert-dialog-action.svelte | 13 ++ .../alert-dialog/alert-dialog-cancel.svelte | 17 +++ .../alert-dialog/alert-dialog-content.svelte | 26 ++++ .../alert-dialog-description.svelte | 16 +++ .../alert-dialog/alert-dialog-footer.svelte | 20 +++ .../alert-dialog/alert-dialog-header.svelte | 20 +++ .../alert-dialog/alert-dialog-overlay.svelte | 19 +++ .../ui/alert-dialog/alert-dialog-title.svelte | 18 +++ src/lib/components/ui/alert-dialog/index.ts | 40 ++++++ .../components/ui/card/card-content.svelte | 16 +++ .../ui/card/card-description.svelte | 16 +++ src/lib/components/ui/card/card-footer.svelte | 16 +++ src/lib/components/ui/card/card-header.svelte | 16 +++ src/lib/components/ui/card/card-title.svelte | 25 ++++ src/lib/components/ui/card/card.svelte | 20 +++ src/lib/components/ui/card/index.ts | 22 +++ src/lib/components/ui/skeleton/index.ts | 7 + .../components/ui/skeleton/skeleton.svelte | 17 +++ src/lib/server/PlayerState.svelte.ts | 110 +++++++++----- src/lib/types.ts | 1 + src/lib/utils.ts | 45 ++++++ src/routes/+layout.svelte | 4 +- src/routes/+page.server.ts | 135 +++++++++++------- src/routes/+page.svelte | 85 ++++++++++- src/routes/api/checkSolve/+server.ts | 12 ++ 26 files changed, 674 insertions(+), 120 deletions(-) create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-action.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-content.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-description.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-header.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-title.svelte create mode 100644 src/lib/components/ui/alert-dialog/index.ts create mode 100644 src/lib/components/ui/card/card-content.svelte create mode 100644 src/lib/components/ui/card/card-description.svelte create mode 100644 src/lib/components/ui/card/card-footer.svelte create mode 100644 src/lib/components/ui/card/card-header.svelte create mode 100644 src/lib/components/ui/card/card-title.svelte create mode 100644 src/lib/components/ui/card/card.svelte create mode 100644 src/lib/components/ui/card/index.ts create mode 100644 src/lib/components/ui/skeleton/index.ts create mode 100644 src/lib/components/ui/skeleton/skeleton.svelte create mode 100644 src/routes/api/checkSolve/+server.ts diff --git a/src/lib/components/DNDGroup.svelte b/src/lib/components/DNDGroup.svelte index e4a4079..2fd8d4e 100644 --- a/src/lib/components/DNDGroup.svelte +++ b/src/lib/components/DNDGroup.svelte @@ -1,6 +1,9 @@ <script lang="ts"> + import * as Card from '$lib/components/ui/card/index.js'; import { dndzone } from 'svelte-dnd-action'; import { flip } from 'svelte/animate'; + import { expoOut } from 'svelte/easing'; + import { truncate } from '$lib/utils'; let { items = $bindable(), image = false, type = 'default' } = $props(); @@ -15,25 +18,44 @@ </script> <section - use:dndzone={{ items, flipDurationMs, type: type }} + use:dndzone={{ + items, + flipDurationMs, + type: type, + dropTargetStyle: { outline: 'none' } + }} onconsider={handleDndConsider} onfinalize={handleDndFinalize} - class="grid grid-cols-3" + class="grid grid-cols-3 items-center gap-16" > - {#if image} - {#each items as item, i (item.id)} - <div animate:flip={{ duration: flipDurationMs }}> - <img class="object-cover" alt="Album Art" src={item.value} /> - <input type="hidden" name="{type}_{i}" value={item.value} /> - </div> - {/each} - {:else} - {#each items as item, i (item.id)} - <div animate:flip={{ duration: flipDurationMs }}> - <p class="text-center">{item.value}</p> - - <input type="hidden" name="{type}_{i}" value={item.value} /> - </div> - {/each} - {/if} + {#each items as item, i (item.id)} + <div animate:flip={{ duration: flipDurationMs, easing: expoOut }}> + <Card.Root + class="overflow-hidden rounded-xl border bg-card text-card-foreground shadow {type === + 'names' + ? 'border-orange-300' + : type === 'artists' + ? 'border-cyan-300' + : ''}" + > + {#if image} + <Card.Content class="p-0"> + <img class="aspect-square w-full object-cover" alt="Album Art" src={item.value} /> + <input type="hidden" name="{type}_{i}" value={item.value} /> + </Card.Content> + {:else} + <Card.Content> + <p class="text-center"> + {#if type === 'artists'} + {truncate(item.value, 30)} + {:else} + {truncate(item.value, 45)} + {/if} + </p> + <input type="hidden" name="{type}_{i}" value={item.value} /> + </Card.Content> + {/if} + </Card.Root> + </div> + {/each} </section> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..715a24f --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,13 @@ +<script lang="ts"> + import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + import { buttonVariants } from "$lib/components/ui/button/index.js"; + import { cn } from "$lib/utils.js"; + + let { + class: className, + ref = $bindable(null), + ...restProps + }: AlertDialogPrimitive.ActionProps = $props(); +</script> + +<AlertDialogPrimitive.Action bind:ref class={cn(buttonVariants(), className)} {...restProps} /> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..e0226fd --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + import { buttonVariants } from "$lib/components/ui/button/index.js"; + import { cn } from "$lib/utils.js"; + + let { + class: className, + ref = $bindable(null), + ...restProps + }: AlertDialogPrimitive.CancelProps = $props(); +</script> + +<AlertDialogPrimitive.Cancel + bind:ref + class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)} + {...restProps} +/> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..858b8bc --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,26 @@ +<script lang="ts"> + import { AlertDialog as AlertDialogPrimitive, type WithoutChild } from "bits-ui"; + import AlertDialogOverlay from "./alert-dialog-overlay.svelte"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + portalProps, + ...restProps + }: WithoutChild<AlertDialogPrimitive.ContentProps> & { + portalProps?: AlertDialogPrimitive.PortalProps; + } = $props(); +</script> + +<AlertDialogPrimitive.Portal {...portalProps}> + <AlertDialogOverlay /> + <AlertDialogPrimitive.Content + bind:ref + class={cn( + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg", + className + )} + {...restProps} + /> +</AlertDialogPrimitive.Portal> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..600ef8c --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + import { cn } from "$lib/utils.js"; + + let { + class: className, + ref = $bindable(null), + ...restProps + }: AlertDialogPrimitive.DescriptionProps = $props(); +</script> + +<AlertDialogPrimitive.Description + bind:ref + class={cn("text-muted-foreground text-sm", className)} + {...restProps} +/> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..91ecaba --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); +</script> + +<div + bind:this={ref} + class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} + {...restProps} +> + {@render children?.()} +</div> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..44a7b08 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); +</script> + +<div + bind:this={ref} + class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} + {...restProps} +> + {@render children?.()} +</div> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..62acab8 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + import { cn } from "$lib/utils.js"; + + let { + class: className, + ref = $bindable(null), + ...restProps + }: AlertDialogPrimitive.OverlayProps = $props(); +</script> + +<AlertDialogPrimitive.Overlay + bind:ref + class={cn( + "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", + className + )} + {...restProps} +/> diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..ef197dc --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,18 @@ +<script lang="ts"> + import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + import { cn } from "$lib/utils.js"; + + let { + class: className, + level = 3, + ref = $bindable(null), + ...restProps + }: AlertDialogPrimitive.TitleProps = $props(); +</script> + +<AlertDialogPrimitive.Title + bind:ref + class={cn("text-lg font-semibold", className)} + {level} + {...restProps} +/> diff --git a/src/lib/components/ui/alert-dialog/index.ts b/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..dd20f99 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,40 @@ +import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +const Root = AlertDialogPrimitive.Root; +const Trigger = AlertDialogPrimitive.Trigger; +const Portal = AlertDialogPrimitive.Portal; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..1f52856 --- /dev/null +++ b/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); +</script> + +<div bind:this={ref} class={cn("p-6", className)} {...restProps}> + {@render children?.()} +</div> diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..da02664 --- /dev/null +++ b/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props(); +</script> + +<p bind:this={ref} class={cn("text-muted-foreground text-sm", className)} {...restProps}> + {@render children?.()} +</p> diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..6894149 --- /dev/null +++ b/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); +</script> + +<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}> + {@render children?.()} +</div> diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..1baa92c --- /dev/null +++ b/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); +</script> + +<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...restProps}> + {@render children?.()} +</div> diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..a201620 --- /dev/null +++ b/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,25 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + level = 3, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { + level?: 1 | 2 | 3 | 4 | 5 | 6; + } = $props(); +</script> + +<div + role="heading" + aria-level={level} + bind:this={ref} + class={cn("font-semibold leading-none tracking-tight", className)} + {...restProps} +> + {@render children?.()} +</div> diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..c7531d5 --- /dev/null +++ b/src/lib/components/ui/card/card.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + import type { WithElementRef } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + children, + ...restProps + }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); +</script> + +<div + bind:this={ref} + class={cn("bg-card text-card-foreground rounded-xl border shadow", className)} + {...restProps} +> + {@render children?.()} +</div> diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..0f9084d --- /dev/null +++ b/src/lib/components/ui/card/index.ts @@ -0,0 +1,22 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, +}; diff --git a/src/lib/components/ui/skeleton/index.ts b/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/src/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..1f8bbc5 --- /dev/null +++ b/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + import type { WithElementRef, WithoutChildren } from "bits-ui"; + import type { HTMLAttributes } from "svelte/elements"; + import { cn } from "$lib/utils.js"; + + let { + ref = $bindable(null), + class: className, + ...restProps + }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props(); +</script> + +<div + bind:this={ref} + class={cn("bg-primary/10 animate-pulse rounded-md", className)} + {...restProps} +></div> diff --git a/src/lib/server/PlayerState.svelte.ts b/src/lib/server/PlayerState.svelte.ts index a8498cd..eb3afa9 100644 --- a/src/lib/server/PlayerState.svelte.ts +++ b/src/lib/server/PlayerState.svelte.ts @@ -2,59 +2,91 @@ import type { Player } from '$lib/types'; import { getContext, setContext } from 'svelte'; export class PlayerState { - players = $state<Player[]>([]); + players = $state<Player[]>([]); - newPlayer(id: string) { - this.players.push({ id: id, stage: 0, highscore: 0 }); - } + newPlayer(id: string) { + this.players.push({ id: id, stage: 0, highscore: 0, playing: true }); + } - getStage(id: string) { - const player = this.players.find((player) => player.id === id); + getStage(id: string) { + const player = this.players.find((player) => player.id === id); - if (!player) { - return undefined; - } + if (!player) { + return undefined; + } - return player.stage; - } + return player.stage; + } - nextStage(id: string) { - const player = this.players.find((player) => player.id === id); + nextStage(id: string) { + const player = this.players.find((player) => player.id === id); - if (!player) { - return; - } + if (!player) { + return; + } - player.stage += 1; - } + player.stage += 1; + } - score(id: string, win: boolean) { - const player = this.players.find((player) => player.id === id); + restart(id: string) { + const player = this.players.find((player) => player.id === id); - if (!player) { - return; - } + if (!player) { + return; + } - if (win) { - player.stage += 1; + player.stage = 0; + player.playing = true; + } - if (player.stage > player.highscore) { - player.highscore = player.stage; - } - } else { - player.stage = 0; - } - } + score(id: string, won: boolean) { + const player = this.players.find((player) => player.id === id); - getHighscore(id: string) { - const player = this.players.find((player) => player.id === id); + if (!player) { + return; + } - if (!player) { - return undefined; - } + if (won) { + player.stage += 1; + return; + } - return player.highscore; - } + player.playing = false; + + if (player.stage > player.highscore) { + player.highscore = player.stage; + } + } + + getHighscore(id: string) { + const player = this.players.find((player) => player.id === id); + + if (!player) { + return undefined; + } + + return player.highscore; + } + + getPlaying(id: string) { + const player = this.players.find((player) => player.id === id); + + if (!player) { + return undefined; + } + + return player.playing; + } + + setPlaying(id: string, playing: boolean) { + const player = this.players.find((player) => player.id === id); + + if (!player) { + return; + } + + player.playing = playing; + } } export const playerState = new PlayerState(); diff --git a/src/lib/types.ts b/src/lib/types.ts index 37d1116..1ef0567 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -8,4 +8,5 @@ export type Player = { id: string; stage: number; highscore: number; + playing: boolean; }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 61d5240..5d47c69 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -60,3 +60,48 @@ export function shuffleArray<T>(array: T[]): T[] { return array; } + +export function truncate(text: string, maxLength: number): string { + // Return original text if it's shorter than or equal to maxLength + if (text.length <= maxLength) { + return text; + } + + const ellipsis = '…'; + const tolerance = 5; + const targetLength = maxLength - 1; // Account for ellipsis + + // Look for spaces within the tolerance range before targetLength + let spaceBeforeIdx = -1; + for (let i = targetLength; i >= targetLength - tolerance; i--) { + if (text[i] === ' ') { + spaceBeforeIdx = i; + break; + } + } + + // Look for spaces within the tolerance range after targetLength + let spaceAfterIdx = -1; + for (let i = targetLength; i <= targetLength + tolerance && i < text.length; i++) { + if (text[i] === ' ') { + spaceAfterIdx = i; + break; + } + } + + // Determine the best cutoff point + let cutoffIndex = targetLength; + if (spaceBeforeIdx !== -1 && spaceAfterIdx !== -1) { + // If we found spaces both before and after, use the closest one + cutoffIndex = + targetLength - spaceBeforeIdx <= spaceAfterIdx - targetLength + ? spaceBeforeIdx + : spaceAfterIdx; + } else if (spaceBeforeIdx !== -1) { + cutoffIndex = spaceBeforeIdx; + } else if (spaceAfterIdx !== -1) { + cutoffIndex = spaceAfterIdx; + } + + return text.slice(0, cutoffIndex).trim() + ellipsis; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9b776b7..5c50b44 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,4 +3,6 @@ let { children } = $props(); </script> -{@render children()} +<div class="flex min-h-screen items-center"> + {@render children()} +</div> diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 3b3d98d..f2c0afc 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -4,80 +4,105 @@ import type { PageServerLoad } from './$types'; import type { AlbumSolveState } from '$lib/types'; import { albumState } from '$lib/server/AlbumState.svelte'; import { playerState } from '$lib/server/PlayerState.svelte'; -import { invalidateAll } from '$app/navigation'; const count = 3; + export const load: PageServerLoad = async ({ fetch, locals }) => { - const { session } = locals; + const { session } = locals; - if (!session?.data?.userId) { - await session.setData({ userId: nanoid() }); - await session.save(); - } + if (!session?.data?.userId) { + await session.setData({ userId: nanoid() }); + await session.save(); + } - const user = session.data.userId; - let stage = playerState.getStage(user); + const user = session.data.userId; + let stage = playerState.getStage(user); - if (!stage) { - playerState.newPlayer(user); - stage = playerState.getStage(user); - } + if (!stage) { + playerState.newPlayer(user); + stage = playerState.getStage(user); + } - const highscore = playerState.getHighscore(user); + const highscore = playerState.getHighscore(user); - const albumData = await fetch(`/api/getAlbums/${count}`) - .then((res) => { - return res.json(); - }) - .then((data) => { - return data; - }); + if (!playerState.getPlaying(user)) { + return { + stage: stage, + highscore: highscore, + playing: false + }; + } - const albumNames = albumData.albums.map((album) => ({ id: nanoid(), value: album.name })); - const albumImages = albumData.albums.map((album) => ({ - id: nanoid(), - value: album.images.at(0).url - })); - const albumArtists = albumData.albums.map((album) => ({ - id: nanoid(), - value: album.artists.map((artist) => artist.name).join(', ') - })); + const albumData = fetch(`/api/getAlbums/${count}`) + .then((res) => { + return res.json(); + }) + .then((data) => { + const albumNames = data.albums.map((album) => ({ id: nanoid(), value: album.name })); + const albumImages = data.albums.map((album) => ({ + id: nanoid(), + value: album.images.at(0).url + })); + const albumArtists = data.albums.map((album) => ({ + id: nanoid(), + value: album.artists.map((artist) => artist.name).join(', ') + })); - return { - names: shuffleArray(albumNames), - images: shuffleArray(albumImages), - artists: shuffleArray(albumArtists), - stage: stage, - highscore: highscore - }; + return { + names: shuffleArray(albumNames), + images: shuffleArray(albumImages), + artists: shuffleArray(albumArtists) + }; + }); + + return { + stage: stage, + highscore: highscore, + playing: true, + streamed: { albums: albumData } + }; }; export const actions = { - default: async ({ request, locals }) => { - const { session } = locals; // you can access `locals.session` + submit: async ({ request, locals }) => { + const { session } = locals; + if (!session?.data?.userId) { + return; + } - if (!session?.data?.userId) { - return; - } + const user = session.data.userId; - const user = session.data.userId; - const data = await request.formData(); + const data = await request.formData(); - const state: AlbumSolveState[] = []; + const state: AlbumSolveState[] = []; - for (let i = 0; i < count; i++) { - const name = data.get(`names_${i}`); - const image = data.get(`images_${i}`); - const artists = data.get(`artists_${i}`); + for (let i = 0; i < count; i++) { + const name = data.get(`names_${i}`); + const image = data.get(`images_${i}`); + const artists = data.get(`artists_${i}`); - const artistList = artists.split(', '); + const artistList = artists.split(', '); - state.push({ name: name, imageUrl: image, artists: artistList }); - } + state.push({ name: name, imageUrl: image, artists: artistList }); + } - const solved = albumState.checkSolve(state); + const solved = albumState.checkSolve(state); - playerState.score(user, solved); - return { loading: false }; - } + playerState.score(user, solved); + + return { solved: solved }; + }, + + restart: async ({ locals }) => { + const { session } = locals; + if (!session?.data?.userId) { + return; + } + + const user = session.data.userId; + + playerState.restart(user); + + return { solved: undefined }; + } }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ef5b526..60fa13b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,22 +1,93 @@ <script lang="ts"> import { Button } from '$lib/components/ui/button/index.js'; + import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js'; + import { Skeleton } from '$lib/components/ui/skeleton/index.js'; import DndGroup from '$lib/components/DNDGroup.svelte'; import type { PageData } from './$types'; import { enhance } from '$app/forms'; - import { invalidateAll } from '$app/navigation'; - let { data }: { data: PageData } = $props(); + let { data, form }: { data: PageData; form: FormData } = $props(); + + let loading = $state(true); + + let names: string[] | undefined = $state(); + let artists: string[] | undefined = $state(); + let images: string[] | undefined = $state(); + + $effect(() => { + loading = false; + console.log(form); + }); + $inspect(data); </script> -<form method="POST" use:enhance class="grid gap-8"> - <DndGroup items={data.names} type="names"></DndGroup> - <DndGroup items={data.artists} type="artists"></DndGroup> - <DndGroup items={data.images} image type="images"></DndGroup> +<AlertDialog.Root open={form?.solved === false}> + <AlertDialog.Content> + <AlertDialog.Header> + <AlertDialog.Title> + {#if data?.highscore && data?.stage && data.highscore === data.stage} + New high score! + {:else} + Maybe next time + {/if} + </AlertDialog.Title> + <AlertDialog.Description> + {#if data.stage === 0} + That's tough. <strong>0 right answers.</strong> + {:else} + You got it right <strong>{data.stage} times.</strong> + {/if} + </AlertDialog.Description> + </AlertDialog.Header> + <AlertDialog.Footer> + <form action="?/restart" method="POST" use:enhance> + <AlertDialog.Action type="submit">Try again</AlertDialog.Action> + </form> + </AlertDialog.Footer> + </AlertDialog.Content> +</AlertDialog.Root> + +<form + action="?/submit" + method="POST" + use:enhance + class="mx-auto grid w-full max-w-3xl gap-8 px-8 transition-all {loading || data?.playing === false + ? 'grayscale' + : ''}" + onsubmit={() => { + loading = true; + }} +> + {#await data.streamed.albums} + {#each { length: 2 } as _} + <section class="grid grid-cols-3 items-center gap-16"> + {#each { length: 3 } as _} + <Skeleton class="h-[6rem] w-full rounded-xl " /> + {/each} + </section> + {/each} + + <section class="grid grid-cols-3 items-center gap-16"> + {#each { length: 3 } as _} + <Skeleton class="aspect-square h-auto max-w-full rounded-xl object-cover" /> + {/each} + </section> + {:then albums} + {#if albums.names && albums.artists && albums.images} + <DndGroup items={albums.names} type="names"></DndGroup> + <DndGroup items={albums.artists} type="artists"></DndGroup> + <DndGroup items={albums.images} image type="images"></DndGroup> + {:else} + <DndGroup items={names} type="names"></DndGroup> + <DndGroup items={artists} type="artists"></DndGroup> + <DndGroup items={images} image type="images"></DndGroup> + {/if} + {/await} <div class="flex justify-evenly"> <p>Stage: {data.stage}</p> - <Button type="submit" variant="outline" onsubmit={() => invalidateAll()}>Submit</Button> + <Button type="submit" variant="outline">Submit</Button> <p>High Score: {data.highscore}</p> </div> </form> diff --git a/src/routes/api/checkSolve/+server.ts b/src/routes/api/checkSolve/+server.ts new file mode 100644 index 0000000..a5c31e5 --- /dev/null +++ b/src/routes/api/checkSolve/+server.ts @@ -0,0 +1,12 @@ +import type { AlbumSolveState } from '$lib/types'; +import { albumState } from '$lib/server/AlbumState.svelte'; +import { spotifyAPI } from '$lib/server/Spotify.svelte'; +import { json } from '@sveltejs/kit'; + +export async function POST({ request }) { + const { state }: { state: AlbumSolveState[] } = request.json(); + + const albums: SpotifyApi.AlbumObjectSimplified[] = []; + + return json({ albums: albums }); +}