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">
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
|
|
|
@ -8,4 +8,5 @@ export type Player = {
|
|||
id: string;
|
||||
stage: number;
|
||||
highscore: number;
|
||||
playing: boolean;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -3,4 +3,6 @@
|
|||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
<div class="flex min-h-screen items-center">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
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