Better styling and loading logic, save state for user session
This commit is contained in:
parent
19127be9a2
commit
26b6c63caf
26 changed files with 674 additions and 120 deletions
|
@ -1,6 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
import { dndzone } from 'svelte-dnd-action';
|
import { dndzone } from 'svelte-dnd-action';
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
|
import { expoOut } from 'svelte/easing';
|
||||||
|
import { truncate } from '$lib/utils';
|
||||||
|
|
||||||
let { items = $bindable(), image = false, type = 'default' } = $props();
|
let { items = $bindable(), image = false, type = 'default' } = $props();
|
||||||
|
|
||||||
|
@ -15,25 +18,44 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
use:dndzone={{ items, flipDurationMs, type: type }}
|
use:dndzone={{
|
||||||
|
items,
|
||||||
|
flipDurationMs,
|
||||||
|
type: type,
|
||||||
|
dropTargetStyle: { outline: 'none' }
|
||||||
|
}}
|
||||||
onconsider={handleDndConsider}
|
onconsider={handleDndConsider}
|
||||||
onfinalize={handleDndFinalize}
|
onfinalize={handleDndFinalize}
|
||||||
class="grid grid-cols-3"
|
class="grid grid-cols-3 items-center gap-16"
|
||||||
|
>
|
||||||
|
{#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}
|
{#if image}
|
||||||
{#each items as item, i (item.id)}
|
<Card.Content class="p-0">
|
||||||
<div animate:flip={{ duration: flipDurationMs }}>
|
<img class="aspect-square w-full object-cover" alt="Album Art" src={item.value} />
|
||||||
<img class="object-cover" alt="Album Art" src={item.value} />
|
|
||||||
<input type="hidden" name="{type}_{i}" value={item.value} />
|
<input type="hidden" name="{type}_{i}" value={item.value} />
|
||||||
</div>
|
</Card.Content>
|
||||||
{/each}
|
|
||||||
{:else}
|
{:else}
|
||||||
{#each items as item, i (item.id)}
|
<Card.Content>
|
||||||
<div animate:flip={{ duration: flipDurationMs }}>
|
<p class="text-center">
|
||||||
<p class="text-center">{item.value}</p>
|
{#if type === 'artists'}
|
||||||
|
{truncate(item.value, 30)}
|
||||||
|
{:else}
|
||||||
|
{truncate(item.value, 45)}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
<input type="hidden" name="{type}_{i}" value={item.value} />
|
<input type="hidden" name="{type}_{i}" value={item.value} />
|
||||||
|
</Card.Content>
|
||||||
|
{/if}
|
||||||
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -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} />
|
|
@ -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}
|
||||||
|
/>
|
|
@ -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>
|
|
@ -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}
|
||||||
|
/>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
||||||
|
/>
|
18
src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
Normal file
18
src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
Normal file
|
@ -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}
|
||||||
|
/>
|
40
src/lib/components/ui/alert-dialog/index.ts
Normal file
40
src/lib/components/ui/alert-dialog/index.ts
Normal file
|
@ -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,
|
||||||
|
};
|
16
src/lib/components/ui/card/card-content.svelte
Normal file
16
src/lib/components/ui/card/card-content.svelte
Normal file
|
@ -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>
|
16
src/lib/components/ui/card/card-description.svelte
Normal file
16
src/lib/components/ui/card/card-description.svelte
Normal file
|
@ -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>
|
16
src/lib/components/ui/card/card-footer.svelte
Normal file
16
src/lib/components/ui/card/card-footer.svelte
Normal file
|
@ -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>
|
16
src/lib/components/ui/card/card-header.svelte
Normal file
16
src/lib/components/ui/card/card-header.svelte
Normal file
|
@ -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>
|
25
src/lib/components/ui/card/card-title.svelte
Normal file
25
src/lib/components/ui/card/card-title.svelte
Normal file
|
@ -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>
|
20
src/lib/components/ui/card/card.svelte
Normal file
20
src/lib/components/ui/card/card.svelte
Normal file
|
@ -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>
|
22
src/lib/components/ui/card/index.ts
Normal file
22
src/lib/components/ui/card/index.ts
Normal file
|
@ -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,
|
||||||
|
};
|
7
src/lib/components/ui/skeleton/index.ts
Normal file
7
src/lib/components/ui/skeleton/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Root from "./skeleton.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Skeleton,
|
||||||
|
};
|
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
|
@ -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>
|
|
@ -5,7 +5,7 @@ export class PlayerState {
|
||||||
players = $state<Player[]>([]);
|
players = $state<Player[]>([]);
|
||||||
|
|
||||||
newPlayer(id: string) {
|
newPlayer(id: string) {
|
||||||
this.players.push({ id: id, stage: 0, highscore: 0 });
|
this.players.push({ id: id, stage: 0, highscore: 0, playing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
getStage(id: string) {
|
getStage(id: string) {
|
||||||
|
@ -28,22 +28,34 @@ export class PlayerState {
|
||||||
player.stage += 1;
|
player.stage += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
score(id: string, win: boolean) {
|
restart(id: string) {
|
||||||
const player = this.players.find((player) => player.id === id);
|
const player = this.players.find((player) => player.id === id);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (win) {
|
player.stage = 0;
|
||||||
|
player.playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
score(id: string, won: boolean) {
|
||||||
|
const player = this.players.find((player) => player.id === id);
|
||||||
|
|
||||||
|
if (!player) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (won) {
|
||||||
player.stage += 1;
|
player.stage += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.playing = false;
|
||||||
|
|
||||||
if (player.stage > player.highscore) {
|
if (player.stage > player.highscore) {
|
||||||
player.highscore = player.stage;
|
player.highscore = player.stage;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
player.stage = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHighscore(id: string) {
|
getHighscore(id: string) {
|
||||||
|
@ -55,6 +67,26 @@ export class PlayerState {
|
||||||
|
|
||||||
return player.highscore;
|
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();
|
export const playerState = new PlayerState();
|
||||||
|
|
|
@ -8,4 +8,5 @@ export type Player = {
|
||||||
id: string;
|
id: string;
|
||||||
stage: number;
|
stage: number;
|
||||||
highscore: number;
|
highscore: number;
|
||||||
|
playing: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,3 +60,48 @@ export function shuffleArray<T>(array: T[]): T[] {
|
||||||
|
|
||||||
return array;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -3,4 +3,6 @@
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="flex min-h-screen items-center">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
|
@ -4,9 +4,9 @@ import type { PageServerLoad } from './$types';
|
||||||
import type { AlbumSolveState } from '$lib/types';
|
import type { AlbumSolveState } from '$lib/types';
|
||||||
import { albumState } from '$lib/server/AlbumState.svelte';
|
import { albumState } from '$lib/server/AlbumState.svelte';
|
||||||
import { playerState } from '$lib/server/PlayerState.svelte';
|
import { playerState } from '$lib/server/PlayerState.svelte';
|
||||||
import { invalidateAll } from '$app/navigation';
|
|
||||||
|
|
||||||
const count = 3;
|
const count = 3;
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
const { session } = locals;
|
const { session } = locals;
|
||||||
|
|
||||||
|
@ -25,20 +25,25 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
|
|
||||||
const highscore = playerState.getHighscore(user);
|
const highscore = playerState.getHighscore(user);
|
||||||
|
|
||||||
const albumData = await fetch(`/api/getAlbums/${count}`)
|
if (!playerState.getPlaying(user)) {
|
||||||
|
return {
|
||||||
|
stage: stage,
|
||||||
|
highscore: highscore,
|
||||||
|
playing: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumData = fetch(`/api/getAlbums/${count}`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data;
|
const albumNames = data.albums.map((album) => ({ id: nanoid(), value: album.name }));
|
||||||
});
|
const albumImages = data.albums.map((album) => ({
|
||||||
|
|
||||||
const albumNames = albumData.albums.map((album) => ({ id: nanoid(), value: album.name }));
|
|
||||||
const albumImages = albumData.albums.map((album) => ({
|
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
value: album.images.at(0).url
|
value: album.images.at(0).url
|
||||||
}));
|
}));
|
||||||
const albumArtists = albumData.albums.map((album) => ({
|
const albumArtists = data.albums.map((album) => ({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
value: album.artists.map((artist) => artist.name).join(', ')
|
value: album.artists.map((artist) => artist.name).join(', ')
|
||||||
}));
|
}));
|
||||||
|
@ -46,21 +51,27 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
return {
|
return {
|
||||||
names: shuffleArray(albumNames),
|
names: shuffleArray(albumNames),
|
||||||
images: shuffleArray(albumImages),
|
images: shuffleArray(albumImages),
|
||||||
artists: shuffleArray(albumArtists),
|
artists: shuffleArray(albumArtists)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
stage: stage,
|
stage: stage,
|
||||||
highscore: highscore
|
highscore: highscore,
|
||||||
|
playing: true,
|
||||||
|
streamed: { albums: albumData }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ request, locals }) => {
|
submit: async ({ request, locals }) => {
|
||||||
const { session } = locals; // you can access `locals.session`
|
const { session } = locals;
|
||||||
|
|
||||||
if (!session?.data?.userId) {
|
if (!session?.data?.userId) {
|
||||||
return;
|
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[] = [];
|
||||||
|
@ -78,6 +89,20 @@ export const actions = {
|
||||||
const solved = albumState.checkSolve(state);
|
const solved = albumState.checkSolve(state);
|
||||||
|
|
||||||
playerState.score(user, solved);
|
playerState.score(user, solved);
|
||||||
return { loading: false };
|
|
||||||
|
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 };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +1,93 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
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 DndGroup from '$lib/components/DNDGroup.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { enhance } from '$app/forms';
|
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);
|
$inspect(data);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form method="POST" use:enhance class="grid gap-8">
|
<AlertDialog.Root open={form?.solved === false}>
|
||||||
<DndGroup items={data.names} type="names"></DndGroup>
|
<AlertDialog.Content>
|
||||||
<DndGroup items={data.artists} type="artists"></DndGroup>
|
<AlertDialog.Header>
|
||||||
<DndGroup items={data.images} image type="images"></DndGroup>
|
<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">
|
<div class="flex justify-evenly">
|
||||||
<p>Stage: {data.stage}</p>
|
<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>
|
<p>High Score: {data.highscore}</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
12
src/routes/api/checkSolve/+server.ts
Normal file
12
src/routes/api/checkSolve/+server.ts
Normal file
|
@ -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 });
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue