Compare commits

..

13 commits

Author SHA1 Message Date
Mihkel Martin Kasterpalu
019ec6cbaa Remove console log 2025-01-21 17:45:58 +02:00
Mihkel Martin Kasterpalu
e7126efb44 Add frontpage, move pakubiiti to subpage. Add new fonts, styling tweaks 2025-01-21 17:39:17 +02:00
Mihkel Martin Kasterpalu
ac3649fae2 Increase max size of main container 2025-01-21 15:30:56 +02:00
Mihkel Martin Kasterpalu
4c8653c42c update .env.example 2025-01-21 15:26:07 +02:00
Mihkel Martin Kasterpalu
734acaf452 remove db scripts from package.json 2025-01-21 15:24:40 +02:00
Mihkel Martin Kasterpalu
486d98302b Remove unneeded orm and db code/packages 2025-01-21 15:24:18 +02:00
Mihkel Martin Kasterpalu
cca4f55166 Add favicon, font, tweak styles 2025-01-21 15:22:19 +02:00
Mihkel Martin Kasterpalu
a50e0d6ff8 Add seperator between names/artists/images, styling tweaks 2025-01-21 13:46:16 +02:00
Mihkel Martin Kasterpalu
10d6751f6f Add border to images 2025-01-21 13:11:06 +02:00
Mihkel Martin Kasterpalu
547e3e5574 Add lucide-svelte 2025-01-21 13:10:59 +02:00
Mihkel Martin Kasterpalu
9418f99897 Add better layout and header 2025-01-21 13:10:47 +02:00
Mihkel Martin Kasterpalu
4ce1b40d7e remove unneeded api page 2025-01-21 13:10:38 +02:00
Mihkel Martin Kasterpalu
5b6f869962 Add sources for external code 2025-01-21 13:10:24 +02:00
32 changed files with 503 additions and 1170 deletions

View file

@ -1 +1,3 @@
DATABASE_URL=local.db
CLIENT_ID=<spotifyAPIID>
CLIENT_SECRET=<spotifyAPISecret>
SESH_SECRET=<longRandomString>

View file

@ -1,14 +0,0 @@
import { defineConfig } from 'drizzle-kit';
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
export default defineConfig({
schema: './src/lib/server/db/schema.ts',
dbCredentials: {
url: process.env.DATABASE_URL
},
verbose: true,
strict: true,
dialect: 'sqlite'
});

View file

@ -10,10 +10,7 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"db:push": "drizzle-kit push",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
@ -23,16 +20,15 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@types/better-sqlite3": "^7.6.12",
"@types/spotify-web-api-node": "^5.0.11",
"autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.78",
"clsx": "^2.1.1",
"drizzle-kit": "^0.30.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
"lucide-svelte": "^0.473.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.10",
@ -48,8 +44,9 @@
"vite": "^5.4.11"
},
"dependencies": {
"better-sqlite3": "^11.8.0",
"drizzle-orm": "^0.38.4",
"@fontsource-variable/kode-mono": "^5.1.1",
"@fontsource-variable/smooch-sans": "^5.1.1",
"mode-watcher": "^0.5.0",
"nanoid": "^5.0.9",
"spotify-web-api-node": "^5.0.2",
"svelte-kit-sessions": "^0.4.0"

930
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -3,73 +3,86 @@
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 94%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 94%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--ring: 24 5.7% 82.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 94%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 94%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 94%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--primary: 60 9.1% 94%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 94%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 94%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 94%;
--ring: 24 5.7% 82.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer components {
.shadow-sharp {
box-shadow: hsl(var(--primary)) 4px 3px 0px;
}
button.shadow-sharp,
a.shadow-sharp {
&:hover {
box-shadow: hsl(var(--primary)) 2px 1px 0px;
translate: 2px 1px;
}
}
}

View file

@ -2,8 +2,13 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Kasterpalu" />
<link rel="manifest" href="/site.webmanifest" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View file

@ -22,21 +22,21 @@
items,
flipDurationMs,
type: type,
dropTargetStyle: { outline: 'none' }
dropTargetStyle: {}
}}
onconsider={handleDndConsider}
onfinalize={handleDndFinalize}
class="grid grid-cols-3 items-center gap-16"
class="grid grid-cols-3 items-center gap-4 sm:gap-8 md:gap-10 lg:gap-14"
>
{#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 ===
class="select-none overflow-hidden rounded-xl border bg-card text-card-foreground shadow shadow-foreground/15 transition-shadow hover:shadow-foreground/25 {type ===
'names'
? 'border-orange-300'
? 'border-red-400 '
: type === 'artists'
? 'border-cyan-300'
: ''}"
? 'border-purple-400 '
: 'border-blue-400'}"
>
{#if image}
<Card.Content class="p-0">
@ -45,7 +45,13 @@
</Card.Content>
{:else}
<Card.Content>
<p class="text-center">
<p
class="text-center {type === 'names'
? 'text-red-900 dark:text-red-200'
: type === 'artists'
? 'text-purple-900 dark:text-purple-200'
: ''}"
>
{#if type === 'artists'}
{truncate(item.value, 30)}
{:else}

View file

@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
orientation = "horizontal",
...restProps
}: SeparatorPrimitive.RootProps = $props();
</script>
<SeparatorPrimitive.Root
bind:ref
class={cn(
"bg-border shrink-0",
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
className
)}
{orientation}
{...restProps}
/>

View file

@ -1,5 +1,4 @@
import type { Player } from '$lib/types';
import { getContext, setContext } from 'svelte';
export class PlayerState {
players = $state<Player[]>([]);

View file

@ -17,7 +17,6 @@ class SpotifyAPI {
return await this.api.clientCredentialsGrant().then(
(data) => {
console.log(data.body);
if (!data.body['expires_in'] || !data.body['access_token']) {
return false;
}

View file

@ -1,6 +0,0 @@
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import { env } from '$env/dynamic/private';
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = new Database(env.DATABASE_URL);
export const db = drizzle(client);

View file

@ -1,6 +0,0 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const user = sqliteTable('user', {
id: integer('id').primaryKey(),
age: integer('age')
});

View file

@ -5,6 +5,7 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// https://perryjanssen.medium.com/getting-random-tracks-using-the-spotify-api-61889b0c0c27
export function getRandomSearch() {
// A list of all characters that can be chosen.
const characters = 'abcdefghijklmnopqrstuvwxyz';
@ -26,6 +27,7 @@ export function getRandomSearch() {
return randomSearch;
}
// Created using Claude 3.5 Sonett
export function shuffleObjectValues<T extends object>(arr: Array<T>): Array<T> {
// Create a copy of the array
const copy = structuredClone(arr);
@ -52,6 +54,7 @@ export function shuffleObjectValues<T extends object>(arr: Array<T>): Array<T> {
return copy;
}
// https://stackoverflow.com/a/12646864
export function shuffleArray<T>(array: T[]): T[] {
for (let i = array.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@ -61,6 +64,7 @@ export function shuffleArray<T>(array: T[]): T[] {
return array;
}
// Created using Claude 3.5 Sonett
export function truncate(text: string, maxLength: number): string {
// Return original text if it's shorter than or equal to maxLength
if (text.length <= maxLength) {

View file

@ -0,0 +1,7 @@
<script lang="ts">
let { children } = $props();
</script>
<div class="container mt-32 flex flex-col items-center py-4">
{@render children()}
</div>

View file

@ -0,0 +1,30 @@
<script lang="ts">
const games = [
{
name: 'Paku biiti',
image: '',
href: 'pakubiiti'
}
];
</script>
<header class="mb-24 flex flex-col items-center font-title">
<h1 class="mb-1 scroll-m-20 text-6xl font-extrabold tracking-tight lg:text-7xl">
stuff.kasterpalu.ee
</h1>
<p class="text-2xl font-semibold text-muted-foreground">Minimängud ja muud huvitavat</p>
</header>
<main class="grid w-full max-w-4xl justify-items-center">
{#each games as { name, image, href }}
<a
class="shadow-sharp flex aspect-[4/1] w-full max-w-sm items-center justify-center rounded-xl border-2 border-current bg-contain bg-no-repeat transition-shadow transition-transform"
style="background-image: url('{image}')"
draggable="false"
{href}
>
<span class="relative block select-none rounded font-mono text-2xl font-semibold lg:text-3xl">
{name}
</span>
</a>
{/each}
</main>

View file

@ -0,0 +1,7 @@
<script lang="ts">
let { children } = $props();
</script>
<div class="container flex min-h-screen flex-col items-center justify-center py-4">
{@render children()}
</div>

View file

@ -33,17 +33,20 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
};
}
const albumData = fetch(`/api/getAlbums/${count}`)
const albumData = fetch(`/api/pakubiiti/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) => ({
const albumNames = data.albums.map((album: SpotifyApi.AlbumObjectSimplified) => ({
id: nanoid(),
value: album.images.at(0).url
value: album.name
}));
const albumArtists = data.albums.map((album) => ({
const albumImages = data.albums.map((album: SpotifyApi.AlbumObjectSimplified) => ({
id: nanoid(),
value: album.images.at(0)?.url || ''
}));
const albumArtists = data.albums.map((album: SpotifyApi.AlbumObjectSimplified) => ({
id: nanoid(),
value: album.artists.map((artist) => artist.name).join(', ')
}));

View file

@ -0,0 +1,125 @@
<script lang="ts">
import { Separator } from '$lib/components/ui/separator/index.js';
import LoaderCircle from 'lucide-svelte/icons/loader-circle';
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';
let { data, form }: { data: PageData; form: FormData } = $props();
let loading = $state(true);
let oldAlbums: SpotifyApi.AlbumObjectSimplified[] = $state([]);
$effect(() => {
// this is a hack to disable grayscale, please ignore
// eslint-disable-next-line no-constant-binary-expression
loading = false && form?.success;
});
// Used when user answers wrong and no new data comes in
$effect(() => {
if (data.streamed?.albums) {
data.streamed.albums.then((data) => {
oldAlbums = data;
});
}
});
</script>
{#snippet footer(loading: boolean)}
<div class="mt-8 flex items-center justify-evenly">
<p class="font-title text-lg font-semibold">Skoor: {data.stage}</p>
{#if loading}
<Button disabled class="min-w-[4.5rem]">
<LoaderCircle class="animate-spin" />
Oota
</Button>
{:else}
<Button type="submit" class="min-w-[4.5rem]">Saada</Button>
{/if}
<p class="font-title text-lg font-semibold">Parim: {data.highscore}</p>
</div>
{/snippet}
<AlertDialog.Root open={form?.solved === false}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>
{#if data?.highscore && data?.stage && data.highscore === data.stage}
Uus parim tulemus!
{:else}
Seekord ei vedanud
{/if}
</AlertDialog.Title>
<AlertDialog.Description>
{#if data.stage === 0}
Põrusid esimesel katsel.
{:else}
Vastasid õigesti <strong>{data.stage} korda.</strong>
{/if}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<form action="?/restart" method="POST" use:enhance>
<AlertDialog.Action type="submit">Uuesti</AlertDialog.Action>
</form>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
<header class="mb-24 flex flex-col items-center font-title">
<h1 class="mb-1 scroll-m-20 text-6xl font-extrabold tracking-tight lg:text-7xl">Paku biiti</h1>
<p class="text-2xl font-semibold text-muted-foreground">
Lohista kokku õiged albumi <span class="text-red-600 dark:text-red-400">nimed</span>,
<span class="text-purple-600 dark:text-purple-400">artistid</span> ja
<span class="text-blue-600 dark:text-blue-400">pildid</span>.
</p>
</header>
<main class="w-full max-w-4xl">
<form
action="?/submit"
method="POST"
use:enhance
class="grid w-full gap-6 transition-all {loading || data?.playing === false ? 'grayscale' : ''}"
>
{#if data?.streamed?.albums}
{#await data.streamed.albums}
{#each { length: 2 } as _}
<section class="grid grid-cols-3 items-center gap-14">
{#each { length: 3 } as _}
<Skeleton class="h-[5.25rem] w-full rounded-xl " />
{/each}
</section>
<Separator />
{/each}
<section class="grid grid-cols-3 items-center gap-14">
{#each { length: 3 } as _}
<Skeleton class="aspect-square h-auto max-w-full rounded-xl object-cover" />
{/each}
</section>
{@render footer(true)}
{:then albums}
<DndGroup items={albums.names} type="names"></DndGroup>
<Separator />
<DndGroup items={albums.artists} type="artists"></DndGroup>
<Separator />
<DndGroup items={albums.images} image type="images"></DndGroup>
{@render footer(false)}
{/await}
{:else}
<DndGroup items={oldAlbums.names} type="names"></DndGroup>
<Separator />
<DndGroup items={oldAlbums.artists} type="artists"></DndGroup>
<Separator />
<DndGroup items={oldAlbums.images} image type="images"></DndGroup>
{@render footer(false)}
{/if}
</form>
</main>

View file

@ -1,8 +1,50 @@
<script lang="ts">
import '../app.css';
import '@fontsource-variable/smooch-sans';
import '@fontsource-variable/kode-mono';
import { ModeWatcher, resetMode, setMode } from 'mode-watcher';
import { Button } from '$lib/components/ui/button/index.js';
import Sun from 'lucide-svelte/icons/sun';
import Moon from 'lucide-svelte/icons/moon';
import LaptopMinimal from 'lucide-svelte/icons/laptop-minimal';
let { children } = $props();
let theme: string = $state('system');
const cycleTheme = () => {
if (theme === 'dark') {
theme = 'light';
setMode('light');
} else if (theme === 'light') {
theme = 'system';
resetMode();
} else {
theme = 'dark';
setMode('dark');
}
};
</script>
<div class="flex min-h-screen items-center">
{@render children()}
</div>
<ModeWatcher />
<header class="container absolute top-0 flex justify-between py-6">
<a href="/">
<img src="/favicon.svg" alt="Mihkel Martin Kasterpalu logo" class="h-10" />
</a>
<Button onclick={() => cycleTheme()} variant="outline" size="icon">
{#if theme === 'dark'}
<Moon class="absolute h-[1.2rem] w-[1.2rem]" />
{:else if theme === 'light'}
<Sun class="h-[1.2rem] w-[1.2rem] " />
{:else}
<LaptopMinimal class="h-[1.2rem] w-[1.2rem]" />
{/if}
<span class="sr-only">Toggle theme</span>
</Button>
</header>
{@render children()}

View file

@ -1,93 +0,0 @@
<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';
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>
<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">Submit</Button>
<p>High Score: {data.highscore}</p>
</div>
</form>

View file

@ -1,12 +0,0 @@
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 });
}

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

18
static/favicon.svg Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1480" height="1480" version="1.1" xmlns="http://www.w3.org/2000/svg">
<svg id="SvgjsSvg1026" class="img-fluid" width="1480" height="1480" cursor="move"
style="transform-origin:50% 50% 0px;transform:matrix(1, 0, 0, 1, -4, -2);transition:none"
viewBox="0 0 14800 14800" xmlns="http://www.w3.org/2000/svg">
<g id="SvgjsG1025" style="transform:none">
<g style="transform:none">
<path id="SvgjsPath1024"
d="m7170 14789c-113-3-232-7-265-8l-60-1 550-550-1300-1301-1300-1300 25 6c26 5 297 69 2204 515l1119 262 877-877v-20c0-11-196-785-434-1720-239-935-433-1701-431-1703 1-2 611 598 1356 1333l1353 1336 913-913c502-502 913-916 913-919 0-6-5948-5922-5959-5927-3-1-377 369-830 822l-823 823 639 2519c352 1386 639 2520 638 2522-2 2-488-114-1081-257-3433-828-3987-961-4002-961h-17l-480 480-479 480-7-8c-4-4-23-68-42-142-142-542-221-1089-236-1635l-6-240 14-500 15-165c58-610 171-1144 357-1700 554-1656 1707-3085 3216-3988 1082-648 2291-1002 3553-1041l245-8 247 8 246 8 169 16c232 22 648 79 661 91 2 1-65 69-149 151-404 393-664 761-823 1165l-46 116-34 134c-19 73-42 174-52 223l-16 90-10 410 16 113c18 120 60 300 74 314l9 9 718-256c395-141 721-258 726-260l7-5-14-53-15-52-12-220 17-85 18-85 58-142 52-78 52-79 85-86 86-86 56-36 55-36 62-21 61-21h44 43l48 16 47 16 60 55 59 55 30 61 30 62 42 160 10 320-204 2608 777 777 2533-2533 107 144c385 515 707 1093 948 1704l75 190 70 215c199 608 307 1171 352 1830l11 155v315 315l-11 155c-70 1025-326 1967-775 2855-825 1630-2206 2890-3904 3559-648 256-1308 412-2036 481l-160 15-190 5c-104 3-224 7-265 8-41 2-167 0-280-4z" />
<path id="SvgjsPath1023"
d="m11586 2508c9-69 65-831 72-987l7-164 167 125c91 69 230 179 308 243l141 119-348 348c-191 191-349 348-350 348s0-15 3-32z" />
</g>
</g>
<g id="SvgjsG1022" fill="#fff" style="transform:none"></g>
</svg>
<style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: invert(100%); } }</style>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

21
static/site.webmanifest Normal file
View file

@ -0,0 +1,21 @@
{
"name": "Kasterpalu",
"short_name": "Kasterpalu",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,96 +1,101 @@
import { fontFamily } from "tailwindcss/defaultTheme";
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import { fontFamily } from 'tailwindcss/defaultTheme';
import type { Config } from 'tailwindcss';
import tailwindcssAnimate from 'tailwindcss-animate';
const config: Config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
darkMode: ['class'],
content: ['./src/**/*.{html,js,svelte,ts}'],
safelist: ['dark'],
theme: {
container: {
center: true,
padding: "2rem",
padding: '2rem',
screens: {
"2xl": "1400px"
'2xl': '1400px'
}
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
border: 'hsl(var(--border) / <alpha-value>)',
input: 'hsl(var(--input) / <alpha-value>)',
ring: 'hsl(var(--ring) / <alpha-value>)',
background: 'hsl(var(--background) / <alpha-value>)',
foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
xl: "calc(var(--radius) + 4px)",
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
xl: 'calc(var(--radius) + 4px)',
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
sans: [...fontFamily.sans]
sans: [...fontFamily.sans],
title: ['Smooch Sans Variable', ...fontFamily.sans],
mono: ['Kode Mono Variable', ...fontFamily.mono]
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--bits-accordion-content-height)' }
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
'accordion-up': {
from: { height: 'var(--bits-accordion-content-height)' },
to: { height: '0' }
},
'caret-blink': {
'0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' }
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'caret-blink': 'caret-blink 1.25s ease-out infinite'
},
transitionTimingFunction: {
default: 'cubic-bezier(0.16, 1, 0.3, 1)'
}
}
},
plugins: [tailwindcssAnimate],
plugins: [tailwindcssAnimate]
};
export default config;