Cleanup code, fix IntelliJ removing star imports, move sessions to Redis.

This commit is contained in:
Bradley Shellnut 2024-11-09 11:05:28 -08:00
parent 88339093e1
commit c9b6269ce9
124 changed files with 1343 additions and 1342 deletions

View file

@ -40,14 +40,17 @@
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2", "arctic": "^1.9.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"drizzle-kit": "^0.27.2", "drizzle-kit": "^0.27.2",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "2.36.0-next.13", "eslint-plugin-svelte": "2.36.0-next.13",
"formsnap": "^1.0.1",
"just-clone": "^6.2.0", "just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0", "just-debounce-it": "^3.2.0",
"lucia": "3.2.0", "lucia": "3.2.0",
"lucide-svelte": "^0.408.0", "lucide-svelte": "^0.408.0",
"mode-watcher": "^0.4.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
@ -61,6 +64,7 @@
"svelte-meta-tags": "^3.1.4", "svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2", "svelte-sequential-preprocessor": "^2.0.2",
"svelte-sonner": "^0.3.28",
"sveltekit-flash-message": "^2.4.4", "sveltekit-flash-message": "^2.4.4",
"sveltekit-superforms": "^2.20.0", "sveltekit-superforms": "^2.20.0",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
@ -97,7 +101,6 @@
"@sveltejs/adapter-node": "^5.2.9", "@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.7", "@sveltejs/adapter-vercel": "^5.4.7",
"@types/feather-icons": "^4.29.4", "@types/feather-icons": "^4.29.4",
"bits-ui": "^0.21.16",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.25.3", "bullmq": "^5.25.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
@ -108,7 +111,6 @@
"drizzle-orm": "^0.36.1", "drizzle-orm": "^0.36.1",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"feather-icons": "^4.29.2", "feather-icons": "^4.29.2",
"formsnap": "^1.0.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hono": "^4.6.9", "hono": "^4.6.9",
"hono-pino": "^0.3.0", "hono-pino": "^0.3.0",
@ -120,7 +122,6 @@
"just-capitalize": "^3.2.0", "just-capitalize": "^3.2.0",
"just-kebab-case": "^4.2.0", "just-kebab-case": "^4.2.0",
"loader": "^2.1.1", "loader": "^2.1.1",
"mode-watcher": "^0.4.1",
"open-props": "^1.7.7", "open-props": "^1.7.7",
"oslo": "^1.2.1", "oslo": "^1.2.1",
"pg": "^8.13.1", "pg": "^8.13.1",
@ -133,7 +134,6 @@
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"stoker": "^1.3.0", "stoker": "^1.3.0",
"svelte-lazy-loader": "^1.0.0", "svelte-lazy-loader": "^1.0.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View file

@ -77,9 +77,6 @@ importers:
'@types/feather-icons': '@types/feather-icons':
specifier: ^4.29.4 specifier: ^4.29.4
version: 4.29.4 version: 4.29.4
bits-ui:
specifier: ^0.21.16
version: 0.21.16(svelte@5.0.0-next.175)
boardgamegeekclient: boardgamegeekclient:
specifier: ^1.9.1 specifier: ^1.9.1
version: 1.9.1 version: 1.9.1
@ -110,9 +107,6 @@ importers:
feather-icons: feather-icons:
specifier: ^4.29.2 specifier: ^4.29.2
version: 4.29.2 version: 4.29.2
formsnap:
specifier: ^1.0.1
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.0(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3))
handlebars: handlebars:
specifier: ^4.7.8 specifier: ^4.7.8
version: 4.7.8 version: 4.7.8
@ -146,9 +140,6 @@ importers:
loader: loader:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
mode-watcher:
specifier: ^0.4.1
version: 0.4.1(svelte@5.0.0-next.175)
open-props: open-props:
specifier: ^1.7.7 specifier: ^1.7.7
version: 1.7.7 version: 1.7.7
@ -185,9 +176,6 @@ importers:
svelte-lazy-loader: svelte-lazy-loader:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
svelte-sonner:
specifier: ^0.3.28
version: 0.3.28(svelte@5.0.0-next.175)
tailwind-merge: tailwind-merge:
specifier: ^2.5.4 specifier: ^2.5.4
version: 2.5.4 version: 2.5.4
@ -255,6 +243,9 @@ importers:
autoprefixer: autoprefixer:
specifier: ^10.4.20 specifier: ^10.4.20
version: 10.4.20(postcss@8.4.47) version: 10.4.20(postcss@8.4.47)
bits-ui:
specifier: ^0.21.16
version: 0.21.16(svelte@5.0.0-next.175)
drizzle-kit: drizzle-kit:
specifier: ^0.27.2 specifier: ^0.27.2
version: 0.27.2 version: 0.27.2
@ -267,6 +258,9 @@ importers:
eslint-plugin-svelte: eslint-plugin-svelte:
specifier: 2.36.0-next.13 specifier: 2.36.0-next.13
version: 2.36.0-next.13(eslint@8.57.1)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) version: 2.36.0-next.13(eslint@8.57.1)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
formsnap:
specifier: ^1.0.1
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.0(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3))
just-clone: just-clone:
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.2.0 version: 6.2.0
@ -279,6 +273,9 @@ importers:
lucide-svelte: lucide-svelte:
specifier: ^0.408.0 specifier: ^0.408.0
version: 0.408.0(svelte@5.0.0-next.175) version: 0.408.0(svelte@5.0.0-next.175)
mode-watcher:
specifier: ^0.4.1
version: 0.4.1(svelte@5.0.0-next.175)
nodemailer: nodemailer:
specifier: ^6.9.16 specifier: ^6.9.16
version: 6.9.16 version: 6.9.16
@ -318,6 +315,9 @@ importers:
svelte-sequential-preprocessor: svelte-sequential-preprocessor:
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.2 version: 2.0.2
svelte-sonner:
specifier: ^0.3.28
version: 0.3.28(svelte@5.0.0-next.175)
sveltekit-flash-message: sveltekit-flash-message:
specifier: ^2.4.4 specifier: ^2.4.4
version: 2.4.4(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175) version: 2.4.4(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)
@ -3834,8 +3834,8 @@ packages:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
object-inspect@1.13.2: object-inspect@1.13.3:
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
obuf@1.1.2: obuf@1.1.2:
@ -8339,7 +8339,7 @@ snapshots:
object-hash@3.0.0: {} object-hash@3.0.0: {}
object-inspect@1.13.2: {} object-inspect@1.13.3: {}
obuf@1.1.2: {} obuf@1.1.2: {}
@ -9122,7 +9122,7 @@ snapshots:
call-bind: 1.0.7 call-bind: 1.0.7
es-errors: 1.3.0 es-errors: 1.3.0
get-intrinsic: 1.2.4 get-intrinsic: 1.2.4
object-inspect: 1.13.2 object-inspect: 1.13.3
siginfo@2.0.0: {} siginfo@2.0.0: {}

10
src/app.d.ts vendored
View file

@ -1,6 +1,6 @@
import type { User } from 'lucia';
import type { ApiClient } from '$lib/server/api'; import type { ApiClient } from '$lib/server/api';
import type { parseApiResponse } from '$lib/utils/api'; import type { parseApiResponse } from '$lib/utils/api';
import type { User } from 'lucia';
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
@ -24,9 +24,9 @@ declare global {
} }
namespace Superforms { namespace Superforms {
type Message = { type Message = {
type: 'error' | 'success' | 'info', type: 'error' | 'success' | 'info';
text: string text: string;
} };
} }
interface Error { interface Error {
code?: string; code?: string;
@ -37,7 +37,7 @@ declare global {
interface Document { interface Document {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // biome-ignore lint/suspicious/noExplicitAny: <explanation>
startViewTransition: (callback: never) => void; // Add your custom property/method here startViewTransition: (callback: never) => void; // Add your custom property/method here
} }
} }

View file

@ -1,16 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow"/>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<meta name="description" content="Bored? Find a game! Bored Game!" /> <meta name="description" content="Bored? Find a game! Bored Game!"/>
<link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg" /> <link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg"/>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width"/>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body> <body>
<div id="svelte">%sveltekit.body%</div> <div id="svelte">%sveltekit.body%</div>
</body> </body>
</html> </html>

View file

@ -1,10 +1,10 @@
import 'reflect-metadata' import 'reflect-metadata';
import { StatusCodes } from '$lib/constants/status-codes' import { StatusCodes } from '$lib/constants/status-codes';
import type { ApiRoutes } from '$lib/server/api' import type { ApiRoutes } from '$lib/server/api';
import { parseApiResponse } from '$lib/utils/api' import { parseApiResponse } from '$lib/utils/api';
import { type Handle, redirect } from '@sveltejs/kit' import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks' import { sequence } from '@sveltejs/kit/hooks';
import { hc } from 'hono/client' import { hc } from 'hono/client';
const apiClient: Handle = async ({ event, resolve }) => { const apiClient: Handle = async ({ event, resolve }) => {
/* ------------------------------ Register api ------------------------------ */ /* ------------------------------ Register api ------------------------------ */
@ -14,29 +14,30 @@ const apiClient: Handle = async ({ event, resolve }) => {
'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(), 'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(),
host: event.request.headers.get('host') || '', host: event.request.headers.get('host') || '',
}, },
}) });
/* ----------------------------- Auth functions ----------------------------- */ /* ----------------------------- Auth functions ----------------------------- */
async function getAuthedUser() { async function getAuthedUser() {
const { data } = await api.user.$get().then(parseApiResponse) const { data } = await api.user.$get().then(parseApiResponse);
return data?.user return data?.user;
} }
async function getAuthedUserOrThrow() { async function getAuthedUserOrThrow() {
const { data } = await api.user.$get().then(parseApiResponse) const { data } = await api.user.$get().then(parseApiResponse);
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/') if (!data || !data.user) {
return data?.user throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
}
return data?.user;
} }
/* ------------------------------ Set contexts ------------------------------ */ /* ------------------------------ Set contexts ------------------------------ */
event.locals.api = api event.locals.api = api;
event.locals.parseApiResponse = parseApiResponse event.locals.parseApiResponse = parseApiResponse;
event.locals.getAuthedUser = getAuthedUser event.locals.getAuthedUser = getAuthedUser;
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
/* ----------------------------- Return response ---------------------------- */ /* ----------------------------- Return response ---------------------------- */
const response = await resolve(event) return await resolve(event);
return response };
}
export const handle: Handle = sequence(apiClient) export const handle: Handle = sequence(apiClient);

View file

@ -1,71 +1,71 @@
<script> <script>
/** /**
* @event {null} save * @event {null} save
* @event {{ prevValue: any; value: any; }} update * @event {{ prevValue: any; value: any; }} update
*/ */
/** /**
* Specify the local storage key * Specify the local storage key
*/ */
export let key = 'local-storage-key'; export let key = 'local-storage-key';
/** /**
* Provide a value to persist * Provide a value to persist
* @type {any} * @type {any}
*/ */
export let value = ''; export let value = '';
/** /**
* Remove the persisted key value from the browser's local storage * Remove the persisted key value from the browser's local storage
* @type {() => void} * @type {() => void}
*/ */
export function clearItem() { export function clearItem() {
localStorage.removeItem(key); localStorage.removeItem(key);
} }
/** /**
* Clear all key values from the browser's local storage * Clear all key values from the browser's local storage
* @type {() => void} * @type {() => void}
*/ */
export function clearAll() { export function clearAll() {
localStorage.clear(); localStorage.clear();
} }
import { onMount, afterUpdate, createEventDispatcher } from 'svelte'; import { afterUpdate, createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let prevValue = value; let prevValue = value;
function setItem() { function setItem() {
if (typeof value === 'object') { if (typeof value === 'object') {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
} else { } else {
localStorage.setItem(key, value); localStorage.setItem(key, value);
} }
} }
onMount(() => { onMount(() => {
const item = localStorage.getItem(key); const item = localStorage.getItem(key);
if (item != null) { if (item != null) {
try { try {
value = JSON.parse(item); value = JSON.parse(item);
} catch (e) { } catch (e) {
value = item; value = item;
} }
} else { } else {
setItem(value); setItem(value);
dispatch('save'); dispatch('save');
} }
}); });
afterUpdate(() => { afterUpdate(() => {
if (prevValue !== value) { if (prevValue !== value) {
setItem(value); setItem(value);
dispatch('update', { prevValue, value }); dispatch('update', { prevValue, value });
} }
prevValue = value; prevValue = value;
}); });
</script> </script>

View file

@ -1,14 +1,15 @@
<script lang="ts"> <script lang="ts">
import type { GameType, SavedGameType } from '$lib/types'; import type { CollectionItems } from '$db/schema';
import type { CollectionItems } from '$db/schema'; import * as Card from '$lib/components/ui/card';
import type { GameType, SavedGameType } from '$lib/types';
export let game: GameType | CollectionItems; export let game: GameType | CollectionItems;
export let variant: 'default' | 'compact' = 'default'; export let variant: 'default' | 'compact' = 'default';
// Naive and assumes description is only on our GameType at the moment // Naive and assumes description is only on our GameType at the moment
function isGameType(game: GameType | SavedGameType): game is GameType { function isGameType(game: GameType | SavedGameType): game is GameType {
return (game as GameType).description !== undefined; return (game as GameType).description !== undefined;
} }
</script> </script>
<article class="grid grid-template-cols-2 gap-4"> <article class="grid grid-template-cols-2 gap-4">

View file

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import Logo from '$components/logo.svelte'; import Logo from '$components/logo.svelte';
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte'; import * as Avatar from '$lib/components/ui/avatar';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte';
let { user = null } = $props(); let { user = null } = $props();
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)'); let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)');
</script> </script>
<header> <header>

View file

@ -1,19 +1,20 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils/ui'; import { cn } from '$lib/utils/ui';
import { type PinInputProps } from 'bits-ui'; import { PinInput } from 'bits-ui';
import { type PinInputProps } from 'bits-ui';
interface Props extends Omit<PinInputProps, 'value'> { interface Props extends Omit<PinInputProps, 'value'> {
value: string; value: string;
inputCount?: number; inputCount?: number;
} }
let { value = $bindable(), inputCount = 6, ...rest }: Props = $props(); let { value = $bindable(), inputCount = 6, ...rest }: Props = $props();
let pin = $state<string[] | undefined>(value?.split('') ?? []); let pin = $state<string[] | undefined>(value?.split('') ?? []);
let inputs = $derived(Array(inputCount).fill(null)); let inputs = $derived(Array(inputCount).fill(null));
$effect(() => { $effect(() => {
value = pin?.join('') ?? ''; value = pin?.join('') ?? '';
}); });
</script> </script>
<PinInput.Root <PinInput.Root

View file

@ -1,17 +1,18 @@
<script lang="ts"> <script lang="ts">
import { type Infer, superForm, type SuperValidated } from 'sveltekit-superforms'; import Checkbox from '$components/ui/checkbox/checkbox.svelte';
import { zodClient } from 'sveltekit-superforms/adapters'; import * as Form from '$components/ui/form';
import { search_schema, type SearchSchema } from '$lib/zodValidation'; import Input from '$components/ui/input/input.svelte';
import Input from '$components/ui/input/input.svelte'; import { type SearchSchema, search_schema } from '$lib/zodValidation';
import Checkbox from '$components/ui/checkbox/checkbox.svelte'; import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
export let data: SuperValidated<Infer<SearchSchema>>; export let data: SuperValidated<Infer<SearchSchema>>;
const form = superForm(data, { const form = superForm(data, {
validators: zodClient(search_schema), validators: zodClient(search_schema),
}); });
const { form: formData } = form; const { form: formData } = form;
</script> </script>
<search> <search>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils'; import type { HTMLAttributes } from "svelte/elements";
import type { HTMLAttributes } from 'svelte/elements'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>; type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils'; import type { HTMLAttributes } from "svelte/elements";
import type { HTMLAttributes } from 'svelte/elements'; import type { HeadingLevel } from "./index.js";
import type { HeadingLevel } from '.'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & { type $$Props = HTMLAttributes<HTMLHeadingElement> & {
level?: HeadingLevel; level?: HeadingLevel;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils'; import type { HTMLAttributes } from "svelte/elements";
import type { HTMLAttributes } from 'svelte/elements'; import { type Variant, alertVariants } from "./index.js";
import { alertVariants, type Variant } from '.'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement> & { type $$Props = HTMLAttributes<HTMLDivElement> & {
variant?: Variant; variant?: Variant;
@ -12,10 +12,6 @@
export { className as class }; export { className as class };
</script> </script>
<div <div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
class={cn(alertVariants({ variant }), className)}
{...$$restProps}
role="alert"
>
<slot /> <slot />
</div> </div>

View file

@ -1,22 +1,22 @@
import {tv, type VariantProps} from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
import Root from "./alert.svelte"; import Root from "./alert.svelte";
import Description from "./alert-description.svelte"; import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte"; import Title from "./alert-title.svelte";
export const alertVariants = tv({ export const alertVariants = tv({
base: "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", base: "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4",
variants: { variants: {
variant: { variant: {
default: "bg-background text-foreground", default: "bg-background text-foreground",
destructive: destructive:
"text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive" "border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive",
} },
}, },
defaultVariants: { defaultVariants: {
variant: "default" variant: "default",
} },
}); });
export type Variant = VariantProps<typeof alertVariants>["variant"]; export type Variant = VariantProps<typeof alertVariants>["variant"];
@ -29,5 +29,5 @@ export {
// //
Root as Alert, Root as Alert,
Description as AlertDescription, Description as AlertDescription,
Title as AlertTitle Title as AlertTitle,
}; };

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { buttonVariants, type Events, type Props } from './index.js'; import { Button as ButtonPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = Props; type $$Props = Props;
type $$Events = Events; type $$Events = Events;

View file

@ -1,5 +1,5 @@
import {tv, type VariantProps} from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
import type {Button as ButtonPrimitive} from "bits-ui"; import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte"; import Root from "./button.svelte";
const buttonVariants = tv({ const buttonVariants = tv({

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from "svelte/elements";
import { cn } from '$lib/utils/ui.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>; type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from "svelte/elements";
import { cn } from '$lib/utils/ui.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLParagraphElement>; type $$Props = HTMLAttributes<HTMLParagraphElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from "svelte/elements";
import { cn } from '$lib/utils/ui.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>; type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from "svelte/elements";
import { cn } from '$lib/utils/ui.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>; type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from './index.js'; import type { HeadingLevel } from "./index.js";
import { cn } from '$lib/utils/ui.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & { type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel; tag?: HeadingLevel;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from "svelte/elements";
import { cn } from '$lib/utils/ui.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLDivElement>; type $$Props = HTMLAttributes<HTMLDivElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from 'lucide-svelte/icons/check'; import Check from "lucide-svelte/icons/check";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps; type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents; type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
@ -14,7 +14,7 @@
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
bind:checked bind:checked
class={cn( class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
{...$$restProps} {...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from '$lib/utils.js'; import { cn, flyAndScale } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.ContentProps; type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents; type $$Events = DropdownMenuPrimitive.ContentEvents;
@ -17,7 +17,7 @@
{transitionConfig} {transitionConfig}
{sideOffset} {sideOffset}
class={cn( class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none", "bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none",
className className
)} )}
{...$$restProps} {...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.ItemProps & { type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean; inset?: boolean;
@ -14,7 +14,7 @@
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
class={cn( class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8", inset && "pl-8",
className className
)} )}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.LabelProps & { type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean; inset?: boolean;

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps; type $$Props = DropdownMenuPrimitive.RadioGroupProps;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from 'lucide-svelte/icons/circle'; import Circle from "lucide-svelte/icons/circle";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps; type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents; type $$Events = DropdownMenuPrimitive.RadioItemEvents;
@ -13,7 +13,7 @@
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
class={cn( class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
{value} {value}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps; type $$Props = DropdownMenuPrimitive.SeparatorProps;
@ -9,6 +9,6 @@
</script> </script>
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)} class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...$$restProps} {...$$restProps}
/> />

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from "svelte/elements";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLSpanElement>; type $$Props = HTMLAttributes<HTMLSpanElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from '$lib/utils.js'; import { cn, flyAndScale } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.SubContentProps; type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents; type $$Events = DropdownMenuPrimitive.SubContentEvents;
@ -18,7 +18,7 @@
{transition} {transition}
{transitionConfig} {transitionConfig}
class={cn( class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none", "bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className className
)} )}
{...$$restProps} {...$$restProps}

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from 'lucide-svelte/icons/chevron-right'; import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & { type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean; inset?: boolean;
@ -15,7 +15,7 @@
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
class={cn( class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground", "data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
inset && "pl-8", inset && "pl-8",
className className
)} )}

View file

@ -1,4 +1,4 @@
import {DropdownMenu as DropdownMenuPrimitive} from "bits-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte"; import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte"; import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte"; import Content from "./dropdown-menu-content.svelte";

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import * as Button from '$lib/components/ui/button/index.js'; import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props; type $$Props = Button.Props;
type $$Events = Button.Events; type $$Events = Button.Events;

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'; import * as FormPrimitive from "formsnap";
import { cn } from '$lib/utils.js'; import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLSpanElement>; type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined; let className: string | undefined | null = undefined;

View file

@ -1,6 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from 'sveltekit-superforms'; import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>; type T = Record<string, unknown>;
type U = FormPathLeaves<T>; type U = FormPathLeaves<T>;
</script> </script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>"> <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap"; import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>; type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import * as FormPrimitive from 'formsnap'; import * as FormPrimitive from "formsnap";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.FieldErrorsProps & { type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null; errorClasses?: string | undefined | null;

View file

@ -1,6 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms'; import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>; type T = Record<string, unknown>;
type U = FormPath<T>; type U = FormPath<T>;
</script> </script>
@ -8,7 +7,7 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>"> <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap"; import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>; type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;

View file

@ -1,13 +1,12 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { FormPath, SuperForm } from 'sveltekit-superforms'; import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>; type T = Record<string, unknown>;
type U = FormPath<T>; type U = FormPath<T>;
</script> </script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>"> <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap"; import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>; type $$Props = FormPrimitive.FieldsetProps<T, U>;

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui'; import type { Label as LabelPrimitive } from "bits-ui";
import { getFormControl } from 'formsnap'; import { getFormControl } from "formsnap";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from "$lib/components/ui/label/index.js";
type $$Props = LabelPrimitive.Props; type $$Props = LabelPrimitive.Props;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import * as FormPrimitive from 'formsnap'; import * as FormPrimitive from "formsnap";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = FormPrimitive.LegendProps; type $$Props = FormPrimitive.LegendProps;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui'; import { Label as LabelPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = LabelPrimitive.Props; type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events; type $$Events = LabelPrimitive.Events;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils.js'; import type { HTMLAttributes } from "svelte/elements";
import type { HTMLAttributes } from 'svelte/elements'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLUListElement>; type $$Props = HTMLAttributes<HTMLUListElement>;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis'; import Ellipsis from "lucide-svelte/icons/ellipsis";
import { cn } from '$lib/utils.js'; import type { HTMLAttributes } from "svelte/elements";
import type { HTMLAttributes } from 'svelte/elements'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLSpanElement>; type $$Props = HTMLAttributes<HTMLSpanElement>;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils.js'; import type { HTMLAttributes } from "svelte/elements";
import type { HTMLAttributes } from 'svelte/elements'; import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLAttributes<HTMLLIElement>; type $$Props = HTMLAttributes<HTMLLIElement>;
let className: $$Props["class"] = undefined; let className: $$Props["class"] = undefined;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui'; import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
import { buttonVariants, type Props } from '$lib/components/ui/button/index.js'; import { type Props, buttonVariants } from "$lib/components/ui/button/index.js";
type $$Props = PaginationPrimitive.PageProps & type $$Props = PaginationPrimitive.PageProps &
Props & { Props & {

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui'; import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRight from 'lucide-svelte/icons/chevron-right'; import ChevronRight from "lucide-svelte/icons/chevron-right";
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from "$lib/components/ui/button/index.js";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = PaginationPrimitive.NextButtonProps; type $$Props = PaginationPrimitive.NextButtonProps;
type $$Events = PaginationPrimitive.NextButtonEvents; type $$Events = PaginationPrimitive.NextButtonEvents;

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui'; import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeft from 'lucide-svelte/icons/chevron-left'; import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from "$lib/components/ui/button/index.js";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = PaginationPrimitive.PrevButtonProps; type $$Props = PaginationPrimitive.PrevButtonProps;
type $$Events = PaginationPrimitive.PrevButtonEvents; type $$Events = PaginationPrimitive.PrevButtonEvents;

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui'; import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = PaginationPrimitive.Props; type $$Props = PaginationPrimitive.Props;
type $$Events = PaginationPrimitive.Events; type $$Events = PaginationPrimitive.Events;
@ -11,7 +11,6 @@
export let perPage: $$Props["perPage"] = 10; export let perPage: $$Props["perPage"] = 10;
export let page: $$Props["page"] = 1; export let page: $$Props["page"] = 1;
export let siblingCount: $$Props["siblingCount"] = 1; export let siblingCount: $$Props["siblingCount"] = 1;
export { className as class }; export { className as class };
$: currentPage = page; $: currentPage = page;

View file

@ -1,4 +1,4 @@
import {Select as SelectPrimitive} from "bits-ui"; import { Select as SelectPrimitive } from "bits-ui";
import Label from "./select-label.svelte"; import Label from "./select-label.svelte";
import Item from "./select-item.svelte"; import Item from "./select-item.svelte";

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from "bits-ui";
import { scale } from 'svelte/transition'; import { scale } from "svelte/transition";
import { cn, flyAndScale } from '$lib/utils.js'; import { cn, flyAndScale } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.ContentProps; type $$Props = SelectPrimitive.ContentProps;
type $$Events = SelectPrimitive.ContentEvents; type $$Events = SelectPrimitive.ContentEvents;
@ -27,7 +27,7 @@
{outTransitionConfig} {outTransitionConfig}
{sideOffset} {sideOffset}
class={cn( class={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md outline-none", "bg-popover text-popover-foreground relative z-50 min-w-[8rem] overflow-hidden rounded-md border shadow-md outline-none",
className className
)} )}
{...$$restProps} {...$$restProps}

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Check from 'lucide-svelte/icons/check'; import Check from "lucide-svelte/icons/check";
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.ItemProps; type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents; type $$Events = SelectPrimitive.ItemEvents;
@ -18,7 +18,7 @@
{disabled} {disabled}
{label} {label}
class={cn( class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
{...$$restProps} {...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.LabelProps; type $$Props = SelectPrimitive.LabelProps;

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.SeparatorProps; type $$Props = SelectPrimitive.SeparatorProps;
@ -8,4 +8,4 @@
export { className as class }; export { className as class };
</script> </script>
<SelectPrimitive.Separator class={cn("-mx-1 my-1 h-px bg-muted", className)} {...$$restProps} /> <SelectPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from 'bits-ui'; import { Select as SelectPrimitive } from "bits-ui";
import ChevronDown from 'lucide-svelte/icons/chevron-down'; import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from '$lib/utils.js'; import { cn } from "$lib/utils/ui.js";
type $$Props = SelectPrimitive.TriggerProps; type $$Props = SelectPrimitive.TriggerProps;
type $$Events = SelectPrimitive.TriggerEvents; type $$Events = SelectPrimitive.TriggerEvents;
@ -12,7 +12,7 @@
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
class={cn( class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "border-input bg-background ring-offset-background focus-visible:ring-ring aria-[invalid]:border-destructive data-[placeholder]:[&>span]:text-muted-foreground flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className className
)} )}
{...$$restProps} {...$$restProps}

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner'; import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from 'mode-watcher'; import { mode } from "mode-watcher";
type $$Props = SonnerProps; type $$Props = SonnerProps;
</script> </script>

View file

@ -1,37 +1,37 @@
<script lang="ts"> <script lang="ts">
import { onNavigate } from '$app/navigation' import { onNavigate } from '$app/navigation';
import { navigating } from '$app/stores' import { navigating } from '$app/stores';
let visible = false let visible = false;
let progress = 0 let progress = 0;
let load_durations: number[] = [] let load_durations: number[] = [];
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length $: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length;
const increment = 1 const increment = 1;
onNavigate((navigation) => { onNavigate((navigation) => {
const typical_load_time = average_load || 200 //ms const typical_load_time = average_load || 200; //ms
const frequency = typical_load_time / 100 const frequency = typical_load_time / 100;
let start = performance.now() let start = performance.now();
// Start the progress bar // Start the progress bar
visible = true visible = true;
progress = 0 progress = 0;
const interval = setInterval(() => { const interval = setInterval(() => {
// Increment the progress bar // Increment the progress bar
progress += increment progress += increment;
}, frequency) }, frequency);
// Resolve the promise when the page is done loading // Resolve the promise when the page is done loading
$navigating?.complete.then(() => { $navigating?.complete.then(() => {
progress = 100 // Fill out the progress bar progress = 100; // Fill out the progress bar
clearInterval(interval) clearInterval(interval);
// after 100 ms hide the progress bar // after 100 ms hide the progress bar
setTimeout(() => { setTimeout(() => {
visible = false visible = false;
}, 500) }, 500);
// Log how long that one took // Log how long that one took
const end = performance.now() const end = performance.now();
const duration = end - start const duration = end - start;
load_durations = [...load_durations, duration] load_durations = [...load_durations, duration];
}) });
}) });
</script> </script>
<div class="progress" class:visible style:--progress={progress}> <div class="progress" class:visible style:--progress={progress}>

View file

@ -1,19 +1,39 @@
import { import { cookieExpiresAt, cookieExpiresMilliseconds, halfCookieExpiresMilliseconds } from '$lib/server/api/common/utils/cookies';
cookieExpiresAt, import { UsersRepository } from '$lib/server/api/repositories/users.repository';
cookieExpiresMilliseconds, import { RedisService } from '$lib/server/api/services/redis.service';
halfCookieExpiresMilliseconds import { sha256 } from '@oslojs/crypto/sha2';
} from '$lib/server/api/common/utils/cookies'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import {SessionsRepository} from '$lib/server/api/repositories/sessions.repository'; import { inject, injectable } from 'tsyringe';
import {sha256} from '@oslojs/crypto/sha2'; import type { Users } from '../databases/postgres/tables';
import {encodeBase32LowerCaseNoPadding, encodeHexLowerCase} from '@oslojs/encoding';
import {inject, injectable} from 'tsyringe';
import type {Sessions, Users} from '../databases/postgres/tables';
export type SessionValidationResult = { session: Sessions; user: Users } | { session: null; user: null }; export type RedisSession = {
id: string;
user_id: string;
expires_at: number;
ip_country: string;
ip_address: string;
two_factor_auth_enabled: boolean;
is_two_factor_authenticated: boolean;
};
export type Session = {
id: string;
userId: string;
expiresAt: Date;
ipCountry: string;
ipAddress: string;
twoFactorAuthEnabled: boolean;
isTwoFactorAuthenticated: boolean;
};
export type SessionValidationResult = { session: Session; user: Users } | { session: null; user: null } | { session: Session; user: undefined };
@injectable() @injectable()
export class SessionsService { export class SessionsService {
constructor(@inject(SessionsRepository) private readonly sessionsRepository: SessionsRepository) {} constructor(
@inject(RedisService) private readonly redisService: RedisService,
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
) {}
generateSessionToken() { generateSessionToken() {
const bytes = new Uint8Array(20); const bytes = new Uint8Array(20);
@ -30,7 +50,7 @@ export class SessionsService {
isTwoFactorAuthenticated: boolean, isTwoFactorAuthenticated: boolean,
) { ) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Sessions = { const session = {
id: sessionId, id: sessionId,
userId, userId,
expiresAt: cookieExpiresAt, expiresAt: cookieExpiresAt,
@ -39,23 +59,49 @@ export class SessionsService {
twoFactorAuthEnabled, twoFactorAuthEnabled,
isTwoFactorAuthenticated, isTwoFactorAuthenticated,
}; };
await this.sessionsRepository.create(session); await this.redisService.client.set(
`session:${sessionId}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: session.expiresAt,
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
return session; return session;
} }
async validateSessionToken(token: string): Promise<SessionValidationResult> { async validateSessionToken(token: string): Promise<SessionValidationResult> {
// TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm // TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm
// const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); // const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const sessions = await this.sessionsRepository.findBySessionId(token); const item = await this.redisService.client.get(`session:${token}`);
if (sessions.length < 1) { if (item === null) {
return { return {
session: null, session: null,
user: null, user: null,
}; };
} }
const { user, session } = sessions[0]; const result: RedisSession = JSON.parse(item);
const session: Session = {
id: result.id,
userId: result.user_id,
expiresAt: new Date(result.expires_at * 1000),
ipCountry: result.ip_country,
ipAddress: result.ip_address,
twoFactorAuthEnabled: result.two_factor_auth_enabled,
isTwoFactorAuthenticated: result.is_two_factor_authenticated,
};
let user: Users | undefined = undefined;
if (session.userId) {
user = await this.usersRepository.findOneById(session.userId);
}
if (Date.now() >= session.expiresAt.getTime()) { if (Date.now() >= session.expiresAt.getTime()) {
await this.sessionsRepository.deleteBySessionId(token); await this.redisService.client.del(`session:${token}`);
return { return {
session: null, session: null,
user: null, user: null,
@ -64,13 +110,26 @@ export class SessionsService {
if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) { if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) {
session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);
await this.sessionsRepository.updateSessionExpiresAt(token, session.expiresAt); await this.redisService.client.set(
`session:${token}`,
JSON.stringify({
id: session.id,
user_id: session.userId,
expires_at: Math.floor(session.expiresAt.getTime() / 1000),
ip_country: session.ipCountry,
ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
} }
return { session, user }; return { session, user };
} }
async invalidateSession(sessionId: string) { async invalidateSession(sessionId: string) {
await this.sessionsRepository.deleteBySessionId(sessionId); await this.redisService.client.del(`session:${sessionId}`);
} }
} }

View file

@ -1,4 +1,4 @@
import { z, ZodNumber, ZodOptional } from 'zod'; import { ZodNumber, ZodOptional, z } from 'zod';
// import zodToJsonSchema from 'zod-to-json-schema'; // import zodToJsonSchema from 'zod-to-json-schema';
export const BoardGameSearch = z.object({ export const BoardGameSearch = z.object({
@ -25,15 +25,7 @@ export type ListGameSchema = typeof list_game_request_schema;
// https://github.com/colinhacks/zod/discussions/330 // https://github.com/colinhacks/zod/discussions/330
export function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) { export function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>(schema: schema) {
return z.preprocess( return z.preprocess((value) => (typeof value === 'string' ? Number.parseInt(value, 10) : typeof value === 'number' ? value : undefined), schema);
(value) =>
typeof value === 'string'
? Number.parseInt(value, 10)
: typeof value === 'number'
? value
: undefined,
schema,
);
} }
const Search = z.object({ const Search = z.object({
@ -73,47 +65,45 @@ export const search_schema = z
limit: z.number().min(10).max(100).default(10), limit: z.number().min(10).max(100).default(10),
skip: z.number().min(0).default(0), skip: z.number().min(0).default(0),
}) })
.superRefine( .superRefine(({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => {
({ minPlayers, maxPlayers, minAge, exactMinAge, exactMinPlayers, exactMaxPlayers }, ctx) => { console.log({ minPlayers, maxPlayers });
console.log({ minPlayers, maxPlayers }); if (minPlayers && maxPlayers && minPlayers > maxPlayers) {
if (minPlayers && maxPlayers && minPlayers > maxPlayers) { ctx.addIssue({
ctx.addIssue({ code: 'custom',
code: 'custom', message: 'Min Players must be smaller than Max Players',
message: 'Min Players must be smaller than Max Players', path: ['minPlayers'],
path: ['minPlayers'], });
}); ctx.addIssue({
ctx.addIssue({ code: 'custom',
code: 'custom', message: 'Min Players must be smaller than Max Players',
message: 'Min Players must be smaller than Max Players', path: ['maxPlayers'],
path: ['maxPlayers'], });
}); }
}
if (exactMinAge && !minAge) { if (exactMinAge && !minAge) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Min Age required when searching for exact min age', message: 'Min Age required when searching for exact min age',
path: ['minAge'], path: ['minAge'],
}); });
} }
if (exactMinPlayers && !minPlayers) { if (exactMinPlayers && !minPlayers) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Min Players required when searching for exact min players', message: 'Min Players required when searching for exact min players',
path: ['minPlayers'], path: ['minPlayers'],
}); });
} }
if (exactMaxPlayers && !maxPlayers) { if (exactMaxPlayers && !maxPlayers) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Max Players required when searching for exact max players', message: 'Max Players required when searching for exact max players',
path: ['maxPlayers'], path: ['maxPlayers'],
}); });
} }
}, });
);
export type SearchSchema = typeof search_schema; export type SearchSchema = typeof search_schema;

View file

@ -1,6 +1,5 @@
import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages'; import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages';
import { db } from '$lib/server/api/packages/drizzle'; import { db } from '$lib/server/api/packages/drizzle';
import { errorMessage } from '$lib/utils/superforms';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { loadFlash, redirect } from 'sveltekit-flash-message/server'; import { loadFlash, redirect } from 'sveltekit-flash-message/server';
import { user_roles } from '../../../../lib/server/api/databases/postgres/tables'; import { user_roles } from '../../../../lib/server/api/databases/postgres/tables';

View file

@ -1,24 +1,22 @@
<script lang="ts"> <script lang="ts">
import { getFlash } from 'sveltekit-flash-message'; import { page } from '$app/stores';
import { page } from '$app/stores'; import { theme } from '$state/theme';
import { onMount } from 'svelte'; import { getFlash } from 'sveltekit-flash-message';
import { theme } from '$state/theme';
import { toastMessage } from '$lib/utils/superforms.js';
const { data } = $props(); const { data } = $props();
const { user } = data; const { user } = data;
const flash = getFlash(page, { const flash = getFlash(page, {
clearOnNavigate: true, clearOnNavigate: true,
clearAfterMs: 3000, clearAfterMs: 3000,
clearArray: true clearArray: true,
}); });
$effect(() => { $effect(() => {
// set the theme to the user's active theme // set the theme to the user's active theme
$theme = user?.theme || 'system'; $theme = user?.theme || 'system';
document.querySelector('html')?.setAttribute('data-theme', $theme); document.querySelector('html')?.setAttribute('data-theme', $theme);
}); });
</script> </script>
<h1>Do the admin stuff</h1> <h1>Do the admin stuff</h1>

View file

@ -1,13 +1,13 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
// const users = await db.query.users.findMany({ // const users = await db.query.users.findMany({
@ -17,5 +17,5 @@ export const load: PageServerLoad = async (event) => {
return { return {
// users, // users,
} };
} };

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import DataTable from './user-table.svelte'; import DataTable from './user-table.svelte';
const { data } = $props();
const { data } = $props();
</script> </script>
<h1>Users</h1> <h1>Users</h1>

View file

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms' import { enhance } from '$app/forms';
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button';
import capitalize from 'just-capitalize' import capitalize from 'just-capitalize';
// import AddRolesForm from './add-rolesTable-form.svelte'; // import AddRolesForm from './add-rolesTable-form.svelte';
const { data } = $props() const { data } = $props();
const { user, availableRoles } = data const { user, availableRoles } = data;
const { user_roles }: { user_roles: { role: { name: string; cuid: string } }[] } = user const { user_roles }: { user_roles: { role: { name: string; cuid: string } }[] } = user;
</script> </script>
<h1>User Details</h1> <h1>User Details</h1>

View file

@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox/index.js' import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import * as Form from '$lib/components/ui/form' import { type AddRoleSchema, addRoleSchema } from '$lib/validations/account';
import { Input } from '$lib/components/ui/input' import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms';
import { type AddRoleSchema, addRoleSchema } from '$lib/validations/account' import { zodClient } from 'sveltekit-superforms/adapters';
import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms'
import { zodClient } from 'sveltekit-superforms/adapters'
export let availableRoles: { name: string; cuid: string }[] = [] export let availableRoles: { name: string; cuid: string }[] = [];
const data: SuperValidated<Infer<AddRoleSchema>> = availableRoles const data: SuperValidated<Infer<AddRoleSchema>> = availableRoles;
const form = superForm(data, { const form = superForm(data, {
validators: zodClient(addRoleSchema), validators: zodClient(addRoleSchema),
@ -18,16 +16,16 @@ const form = superForm(data, {
// toast.error("Please fix the errors in the form."); // toast.error("Please fix the errors in the form.");
// } // }
// } // }
}) });
const { form: formData, enhance } = form const { form: formData, enhance } = form;
function addRole(id: string) { function addRole(id: string) {
$formData.roles = [...$formData.roles, id] $formData.roles = [...$formData.roles, id];
} }
function removeRole(id: string) { function removeRole(id: string) {
$formData.roles = $formData.roles.filter((i) => i !== id) $formData.roles = $formData.roles.filter((i) => i !== id);
} }
</script> </script>

View file

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis'; import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import { User } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button'; import Ellipsis from 'lucide-svelte/icons/ellipsis';
import { User } from 'lucide-svelte';
export let cuid: string; export let cuid: string;
</script> </script>
<DropdownMenu.Root> <DropdownMenu.Root>

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
export let checked: Writable<boolean>; export let checked: Writable<boolean>;
</script> </script>
<Checkbox bind:checked={$checked} /> <Checkbox bind:checked={$checked} />

View file

@ -1,135 +1,114 @@
<script lang="ts"> <script lang="ts">
import { import type { Users } from '$db/schema';
createTable, import { Button } from '$lib/components/ui/button';
Render, import { Input } from '$lib/components/ui/input';
Subscribe, import ArrowUpDown from 'lucide-svelte/icons/arrow-up-down';
createRender, import ChevronDown from 'lucide-svelte/icons/chevron-down';
} from "svelte-headless-table"; import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table';
import { import { addHiddenColumns, addPagination, addSelectedRows, addSortBy, addTableFilter } from 'svelte-headless-table/plugins';
addPagination, import { readable } from 'svelte/store';
addSortBy, import DataTableActions from './user-table-actions.svelte';
addTableFilter, import DataTableCheckbox from './user-table-checkbox.svelte';
addHiddenColumns,
addSelectedRows,
} from "svelte-headless-table/plugins";
import { readable } from "svelte/store";
import ArrowUpDown from "lucide-svelte/icons/arrow-up-down";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import * as Table from "$lib/components/ui/table";
import DataTableActions from "./user-table-actions.svelte";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import DataTableCheckbox from "./user-table-checkbox.svelte";
import type { Users } from '$db/schema';
export let users: Users[] = []; export let users: Users[] = [];
const table = createTable(readable(users), { const table = createTable(readable(users), {
page: addPagination(), page: addPagination(),
sort: addSortBy({ disableMultiSort: true }), sort: addSortBy({ disableMultiSort: true }),
filter: addTableFilter({ filter: addTableFilter({
fn: ({ filterValue, value }) => value.includes(filterValue), fn: ({ filterValue, value }) => value.includes(filterValue),
}), }),
hide: addHiddenColumns(), hide: addHiddenColumns(),
select: addSelectedRows(), select: addSelectedRows(),
}); });
const columns = table.createColumns([ const columns = table.createColumns([
table.column({ table.column({
accessor: "cuid", accessor: 'cuid',
header: (_, { pluginStates }) => { header: (_, { pluginStates }) => {
const { allPageRowsSelected } = pluginStates.select; const { allPageRowsSelected } = pluginStates.select;
return createRender(DataTableCheckbox, { return createRender(DataTableCheckbox, {
checked: allPageRowsSelected, checked: allPageRowsSelected,
}); });
}, },
cell: ({ row }, { pluginStates }) => { cell: ({ row }, { pluginStates }) => {
const { getRowState } = pluginStates.select; const { getRowState } = pluginStates.select;
const { isSelected } = getRowState(row); const { isSelected } = getRowState(row);
return createRender(DataTableCheckbox, { return createRender(DataTableCheckbox, {
checked: isSelected, checked: isSelected,
}); });
}, },
plugins: { plugins: {
filter: { filter: {
exclude: true, exclude: true,
},
},
}),
table.column({
accessor: "username",
header: "Username",
}),
table.column({
accessor: "email",
header: "Email",
cell: ({ value }) => {
return value ?? "N/A";
}
}),
table.column({
accessor: "first_name",
header: "First Name",
cell: ({ value }) => {
return value && value.length > 0 ? value : "N/A";
}, },
plugins: { },
filter: { }),
exclude: true, table.column({
}, accessor: 'username',
}, header: 'Username',
}), }),
table.column({ table.column({
accessor: "last_name", accessor: 'email',
header: "Last Name", header: 'Email',
cell: ({ value }) => { cell: ({ value }) => {
return value && value.length > 0 ? value : "N/A"; return value ?? 'N/A';
},
}),
table.column({
accessor: 'first_name',
header: 'First Name',
cell: ({ value }) => {
return value && value.length > 0 ? value : 'N/A';
},
plugins: {
filter: {
exclude: true,
}, },
plugins: { },
filter: { }),
exclude: true, table.column({
}, accessor: 'last_name',
header: 'Last Name',
cell: ({ value }) => {
return value && value.length > 0 ? value : 'N/A';
},
plugins: {
filter: {
exclude: true,
}, },
}), },
table.column({ }),
accessor: ({ cuid }) => cuid, table.column({
header: "", accessor: ({ cuid }) => cuid,
cell: ({ value }) => { header: '',
return createRender(DataTableActions, { cuid: value }); cell: ({ value }) => {
}, return createRender(DataTableActions, { cuid: value });
plugins: { },
sort: { plugins: {
disable: true, sort: {
}, disable: true,
}, },
}), },
]); }),
]);
const { const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates, flatColumns, rows } = table.createViewModel(columns);
headerRows,
pageRows,
tableAttrs,
tableBodyAttrs,
pluginStates,
flatColumns,
rows,
} = table.createViewModel(columns);
const { pageIndex, hasNextPage, hasPreviousPage } = pluginStates.page; const { pageIndex, hasNextPage, hasPreviousPage } = pluginStates.page;
const { filterValue } = pluginStates.filter; const { filterValue } = pluginStates.filter;
const { hiddenColumnIds } = pluginStates.hide; const { hiddenColumnIds } = pluginStates.hide;
const { selectedDataIds } = pluginStates.select; const { selectedDataIds } = pluginStates.select;
const ids = flatColumns.map((col) => col.id); const ids = flatColumns.map((col) => col.id);
let hideForId = Object.fromEntries(ids.map((id) => [id, true])); let hideForId = Object.fromEntries(ids.map((id) => [id, true]));
$: $hiddenColumnIds = Object.entries(hideForId) $: $hiddenColumnIds = Object.entries(hideForId)
.filter(([, hide]) => !hide) .filter(([, hide]) => !hide)
.map(([id]) => id); .map(([id]) => id);
const columnsToHide: string[] = []; const columnsToHide: string[] = [];
</script> </script>
<div> <div>

View file

@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$components/ui/card' import * as Card from '$components/ui/card';
const { data } = $props();
const { data } = $props() let collections = data?.collections || [];
let collections = data?.collections || []
</script> </script>
<svelte:head> <svelte:head>

View file

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
</script> </script>
<div class="error"> <div class="error">
{#if $page.status === 404} {#if $page.status === 404}
<h1>The page you requested doesn't exist! 🤷‍♂️</h1> <h1>The page you requested doesn't exist! 🤷</h1>
<h3 class="mt-6"><a href="/">Go Home</a></h3> <h3 class="mt-6"><a href="/">Go Home</a></h3>
{:else} {:else}
<h1 class="h1">Unexpected Error</h1> <h1 class="h1">Unexpected Error</h1>

View file

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
// import { tick, onDestroy } from 'svelte'; // import { tick, onDestroy } from 'svelte';
import Game from '$components/Game.svelte' import Game from '$components/Game.svelte';
import type { UICollection } from '$lib/types' import type { UICollection } from '$lib/types';
const { data } = $props() const { data } = $props();
const { items = [] } = data const { items = [] } = data;
console.log(`Page data: ${JSON.stringify(data)}`) console.log(`Page data: ${JSON.stringify(data)}`);
let collection: UICollection = data?.collection ?? {} let collection: UICollection = data?.collection ?? {};
console.log('items', items) console.log('items', items);
// async function handleNextPageEvent(event: CustomEvent) { // async function handleNextPageEvent(event: CustomEvent) {
// if (+event?.detail?.page === page + 1) { // if (+event?.detail?.page === page + 1) {

View file

@ -1,14 +1,13 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import { userNotAuthenticated } from '$lib/server/auth-utils' import { redirect } from 'sveltekit-flash-message/server';
import { redirect } from 'sveltekit-flash-message/server'
export async function load(event) { export async function load(event) {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
return {} return {};
} }

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import GameSearchForm from "$components/search/GameSearchForm.svelte"; import GameSearchForm from '$components/search/GameSearchForm.svelte';
export let data; export let data;
</script> </script>
<h1>Add a game to your collection</h1> <h1>Add a game to your collection</h1>

View file

@ -1,20 +1,19 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import { userNotAuthenticated } from '$lib/server/auth-utils' import { BggForm } from '$lib/zodValidation';
import { BggForm } from '$lib/zodValidation' import { redirect } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit' import { zod } from 'sveltekit-superforms/adapters';
import { zod } from 'sveltekit-superforms/adapters' import { superValidate } from 'sveltekit-superforms/server';
import { superValidate } from 'sveltekit-superforms/server' import type { PageServerLoad } from '../$types';
import type { PageServerLoad } from '../$types'
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
const form = await superValidate({}, zod(BggForm)) const form = await superValidate({}, zod(BggForm));
return { form } return { form };
} };

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Input from "$components/ui/input/input.svelte"; import Input from '$components/ui/input/input.svelte';
import Label from "$components/ui/label/label.svelte"; import Label from '$components/ui/label/label.svelte';
export let data; export let data;
</script> </script>
<h1>Add a game to your collection</h1> <h1>Add a game to your collection</h1>

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Transition from '$components/transition.svelte' import Transition from '$components/transition.svelte';
export let data export let data;
const wishlistsTable = data.wishlists || [] const wishlistsTable = data.wishlists || [];
</script> </script>
<aside class="wishlists"> <aside class="wishlists">

View file

@ -1,6 +1,5 @@
import { notSignedInMessage } from '$lib/flashMessages'; import { notSignedInMessage } from '$lib/flashMessages';
import { db } from '$lib/server/api/packages/drizzle'; import { db } from '$lib/server/api/packages/drizzle';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import { modifyListGameSchema } from '$lib/validations/zod-schemas'; import { modifyListGameSchema } from '$lib/validations/zod-schemas';
import { type Actions, fail } from '@sveltejs/kit'; import { type Actions, fail } from '@sveltejs/kit';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';

View file

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import Game from '$components/Game.svelte' import Game from '$components/Game.svelte';
export let data export let data;
console.log('data', data) console.log('data', data);
const wishlist = data.wishlist const wishlist = data.wishlist;
const gamesItems = wishlist?.items const gamesItems = wishlist?.items;
</script> </script>
<svelte:head> <svelte:head>

View file

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores' import { page } from '$app/stores';
import type { Route } from '$lib/types' import type { Route } from '$lib/types';
const routes: Route[] = [ const routes: Route[] = [
{ href: '/settings/profile', label: 'Profile' }, { href: '/settings/profile', label: 'Profile' },
{ href: '/settings/security', label: 'Security' }, { href: '/settings/security', label: 'Security' },
] ];
let { children } = $props() let { children } = $props();
</script> </script>
<div class="security-nav"> <div class="security-nav">

View file

@ -1,8 +1,8 @@
// +page.server.ts // +page.server.ts
import { redirect } from '@sveltejs/kit' import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
// Redirect to a different page // Redirect to a different page
throw redirect(307, '/settings/profile') throw redirect(307, '/settings/profile');
} };

View file

@ -1,18 +1,18 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
return { return {
hasSetupTwoFactor: authedUser.mfa_enabled, hasSetupTwoFactor: authedUser.mfa_enabled,
} };
} };
export const actions = {} export const actions = {};

View file

@ -1,10 +1,10 @@
<script> <script>
import { Button } from '$components/ui/button/index' import { Button } from '$components/ui/button/index';
import { KeyRound } from 'lucide-svelte' import { KeyRound } from 'lucide-svelte';
const { data } = $props() const { data } = $props();
const hasSetupTwoFactor = data.hasSetupTwoFactor const hasSetupTwoFactor = data.hasSetupTwoFactor;
</script> </script>
<div class="mt-6"> <div class="mt-6">

View file

@ -1,81 +1,81 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import { type Actions, fail } from '@sveltejs/kit' import { type Actions, fail } from '@sveltejs/kit';
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server';
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate } from 'sveltekit-superforms/server' import { setError, superValidate } from 'sveltekit-superforms/server';
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types';
import { changeUserPasswordSchema } from './schemas' import { changeUserPasswordSchema } from './schemas';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
const form = await superValidate(event, zod(changeUserPasswordSchema)) const form = await superValidate(event, zod(changeUserPasswordSchema));
form.data = { form.data = {
current_password: '', current_password: '',
password: '', password: '',
confirm_password: '', confirm_password: '',
} };
return { return {
form, form,
} };
} };
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { default: async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
const form = await superValidate(event, zod(changeUserPasswordSchema)) const form = await superValidate(event, zod(changeUserPasswordSchema));
if (!form.valid) { if (!form.valid) {
return fail(400, { return fail(400, {
form, form,
}) });
} }
const { error: verifyPasswordError } = await locals.api.me.verify.password const { error: verifyPasswordError } = await locals.api.me.verify.password
.$post({ .$post({
json: { password: form.data.current_password }, json: { password: form.data.current_password },
}) })
.then(locals.parseApiResponse) .then(locals.parseApiResponse);
console.log('verifyPasswordError', verifyPasswordError) console.log('verifyPasswordError', verifyPasswordError);
if (verifyPasswordError) { if (verifyPasswordError) {
console.error(verifyPasswordError) console.error(verifyPasswordError);
return setError(form, 'current_password', 'Your password is incorrect') return setError(form, 'current_password', 'Your password is incorrect');
} }
if (authedUser?.username) { if (authedUser?.username) {
try { try {
if (form.data.password !== form.data.confirm_password) { if (form.data.password !== form.data.confirm_password) {
return setError(form, 'Password and confirm password do not match') return setError(form, 'Password and confirm password do not match');
} }
await locals.api.me.update.password.$put({ await locals.api.me.update.password.$put({
json: { password: form.data.password, confirm_password: form.data.confirm_password }, json: { password: form.data.password, confirm_password: form.data.confirm_password },
}) });
} catch (e) { } catch (e) {
console.error(e) console.error(e);
form.data.password = '' form.data.password = '';
form.data.confirm_password = '' form.data.confirm_password = '';
form.data.current_password = '' form.data.current_password = '';
return setError(form, 'current_password', 'Your password is incorrect.') return setError(form, 'current_password', 'Your password is incorrect.');
} }
const message = { const message = {
type: 'success', type: 'success',
message: 'Password Updated. Please sign in.', message: 'Password Updated. Please sign in.',
} as const } as const;
redirect(302, '/login', message, event) redirect(302, '/login', message, event);
} }
return setError(form, 'Error occurred. Please try again or contact support if you need further help.') return setError(form, 'Error occurred. Please try again or contact support if you need further help.');
}, },
} };

View file

@ -1,32 +1,32 @@
<script lang="ts"> <script lang="ts">
import * as Alert from '$components/ui/alert' import * as Alert from '$components/ui/alert';
import * as Form from '$components/ui/form' import * as Form from '$components/ui/form';
import { Input } from '$components/ui/input' import { Input } from '$components/ui/input';
import { Toggle } from '$components/ui/toggle' import { Toggle } from '$components/ui/toggle';
import { AlertTriangle, EyeIcon, EyeOff } from 'lucide-svelte' import { AlertTriangle, EyeIcon, EyeOff } from 'lucide-svelte';
import { zodClient } from 'sveltekit-superforms/adapters' import { zodClient } from 'sveltekit-superforms/adapters';
import { superForm } from 'sveltekit-superforms/client' import { superForm } from 'sveltekit-superforms/client';
import { changeUserPasswordSchema } from './schemas' import { changeUserPasswordSchema } from './schemas';
const { data } = $props() const { data } = $props();
const form = superForm(data.form, { const form = superForm(data.form, {
taintedMessage: null, taintedMessage: null,
validators: zodClient(changeUserPasswordSchema), validators: zodClient(changeUserPasswordSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
}) });
let hiddenCurrentPassword = $state(true) let hiddenCurrentPassword = $state(true);
let hiddenPassword = $state(true) let hiddenPassword = $state(true);
let hiddenConfirmPassword = $state(true) let hiddenConfirmPassword = $state(true);
let currentPasswordInput = $derived(hiddenCurrentPassword ? 'password' : 'text') let currentPasswordInput = $derived(hiddenCurrentPassword ? 'password' : 'text');
let passwordInput = $derived(hiddenPassword ? 'password' : 'text') let passwordInput = $derived(hiddenPassword ? 'password' : 'text');
let confirmPasswordInput = $derived(hiddenConfirmPassword ? 'password' : 'text') let confirmPasswordInput = $derived(hiddenConfirmPassword ? 'password' : 'text');
// $inspect(hiddenCurrentPassword, hiddenPassword, hiddenConfirmPassword) // $inspect(hiddenCurrentPassword, hiddenPassword, hiddenConfirmPassword)
const { form: formData, enhance } = form const { form: formData, enhance } = form;
</script> </script>
<form method="POST" use:enhance> <form method="POST" use:enhance>

View file

@ -1,4 +1,4 @@
import { z } from 'zod' import { z } from 'zod';
export const changeUserPasswordSchema = z export const changeUserPasswordSchema = z
.object({ .object({
@ -7,15 +7,15 @@ export const changeUserPasswordSchema = z
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(), confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
}) })
.superRefine(({ confirm_password, password }, ctx) => { .superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx) refinePasswords(confirm_password, password, ctx);
}) });
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema;
const refinePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => { const refinePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
await comparePasswords(confirm_password, password, ctx) await comparePasswords(confirm_password, password, ctx);
await checkPasswordStrength(password, ctx) await checkPasswordStrength(password, ctx);
} };
const comparePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => { const comparePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
if (confirm_password !== password) { if (confirm_password !== password) {
@ -23,52 +23,52 @@ const comparePasswords = async (confirm_password: string, password: string, ctx:
code: 'custom', code: 'custom',
message: 'Password and Confirm Password must match', message: 'Password and Confirm Password must match',
path: ['confirm_password'], path: ['confirm_password'],
}) });
} }
} };
const checkPasswordStrength = async (password: string, ctx: z.RefinementCtx) => { const checkPasswordStrength = async (password: string, ctx: z.RefinementCtx) => {
const minimumLength = password.length < 8 const minimumLength = password.length < 8;
const maximumLength = password.length > 128 const maximumLength = password.length > 128;
const containsUppercase = (ch: string) => /[A-Z]/.test(ch) const containsUppercase = (ch: string) => /[A-Z]/.test(ch);
const containsLowercase = (ch: string) => /[a-z]/.test(ch) const containsLowercase = (ch: string) => /[a-z]/.test(ch);
const containsSpecialChar = (ch: string) => /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch) const containsSpecialChar = (ch: string) => /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch);
let countOfUpperCase = 0 let countOfUpperCase = 0;
let countOfLowerCase = 0 let countOfLowerCase = 0;
let countOfNumbers = 0 let countOfNumbers = 0;
let countOfSpecialChar = 0 let countOfSpecialChar = 0;
for (let i = 0; i < password.length; i++) { for (let i = 0; i < password.length; i++) {
const char = password.charAt(i) const char = password.charAt(i);
if (!Number.isNaN(+char)) { if (!Number.isNaN(+char)) {
countOfNumbers++ countOfNumbers++;
} else if (containsUppercase(char)) { } else if (containsUppercase(char)) {
countOfUpperCase++ countOfUpperCase++;
} else if (containsLowercase(char)) { } else if (containsLowercase(char)) {
countOfLowerCase++ countOfLowerCase++;
} else if (containsSpecialChar(char)) { } else if (containsSpecialChar(char)) {
countOfSpecialChar++ countOfSpecialChar++;
} }
} }
let errorMessage = 'Your password:' let errorMessage = 'Your password:';
if (countOfLowerCase < 1) { if (countOfLowerCase < 1) {
errorMessage = ' Must have at least one lowercase letter. ' errorMessage = ' Must have at least one lowercase letter. ';
} }
if (countOfNumbers < 1) { if (countOfNumbers < 1) {
errorMessage += ' Must have at least one number. ' errorMessage += ' Must have at least one number. ';
} }
if (countOfUpperCase < 1) { if (countOfUpperCase < 1) {
errorMessage += ' Must have at least one uppercase letter. ' errorMessage += ' Must have at least one uppercase letter. ';
} }
if (countOfSpecialChar < 1) { if (countOfSpecialChar < 1) {
errorMessage += ' Must have at least one special character.' errorMessage += ' Must have at least one special character.';
} }
if (minimumLength) { if (minimumLength) {
errorMessage += ' Be at least 8 characters long.' errorMessage += ' Be at least 8 characters long.';
} }
if (maximumLength) { if (maximumLength) {
errorMessage += ' Be less than 128 characters long.' errorMessage += ' Be less than 128 characters long.';
} }
if (errorMessage.length > 'Your password:'.length) { if (errorMessage.length > 'Your password:'.length) {
@ -76,6 +76,6 @@ const checkPasswordStrength = async (password: string, ctx: z.RefinementCtx) =>
code: 'custom', code: 'custom',
message: errorMessage, message: errorMessage,
path: ['password'], path: ['password'],
}) });
} }
} };

View file

@ -1,24 +1,24 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import type { Actions } from '@sveltejs/kit' import type { Actions } from '@sveltejs/kit';
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from '../../$types' import type { PageServerLoad } from '../../$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
const { data: totpData, error: totpDataError } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse) const { data: totpData, error: totpDataError } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
const totpEnabled = !!totpData const totpEnabled = !!totpData;
return { return {
totpEnabled, totpEnabled,
hardwareTokenEnabled: false, hardwareTokenEnabled: false,
} };
} };
export const actions: Actions = {} export const actions: Actions = {};

View file

@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Badge } from '$components/ui/badge' import { Badge } from '$components/ui/badge';
import { Button } from '$components/ui/button' import { Button } from '$components/ui/button';
import * as Card from '$lib/components/ui/card' import * as Card from '$components/ui/card';
const { data } = $props() const { data } = $props();
const totpEnabled = data.totpEnabled const totpEnabled = data.totpEnabled;
const hardwareTokenEnabled = data.hardwareTokenEnabled const hardwareTokenEnabled = data.hardwareTokenEnabled;
</script> </script>
<h1>Two-factor authentication</h1> <h1>Two-factor authentication</h1>

View file

@ -1,28 +1,28 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from '../../../$types' import type { PageServerLoad } from '../../../$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
if (authedUser.mfa_enabled) { if (authedUser.mfa_enabled) {
const { data: recoveryCodesData, error: recoveryCodesError } = await locals.api.mfa.totp.recoveryCodes.$get().then(locals.parseApiResponse) const { data: recoveryCodesData, error: recoveryCodesError } = await locals.api.mfa.totp.recoveryCodes.$get().then(locals.parseApiResponse);
console.log('recoveryCodesData', recoveryCodesData) console.log('recoveryCodesData', recoveryCodesData);
console.log('recoveryCodesError', recoveryCodesError) console.log('recoveryCodesError', recoveryCodesError);
if (recoveryCodesError || !recoveryCodesData || !recoveryCodesData.recoveryCodes) { if (recoveryCodesError || !recoveryCodesData || !recoveryCodesData.recoveryCodes) {
return { return {
recoveryCodes: [], recoveryCodes: [],
} };
} }
return { return {
recoveryCodes: recoveryCodesData.recoveryCodes, recoveryCodes: recoveryCodesData.recoveryCodes,
} };
} }
redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event) redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event);
} };

View file

@ -0,0 +1,5 @@
<script lang="ts"></script>
<h1>Security Keys</h1>
<p>TODO</p>

View file

@ -1,35 +1,35 @@
import { notSignedInMessage } from '$lib/flashMessages' import { notSignedInMessage } from '$lib/flashMessages';
import env from '$lib/server/api/common/env' import env from '$lib/server/api/common/env';
import { decodeHex, encodeBase32 } from '@oslojs/encoding' import { decodeHex, encodeBase32 } from '@oslojs/encoding';
import { createTOTPKeyURI } from '@oslojs/otp' import { createTOTPKeyURI } from '@oslojs/otp';
import { type Actions, fail } from '@sveltejs/kit' import { type Actions, fail } from '@sveltejs/kit';
import kebabCase from 'just-kebab-case' import kebabCase from 'just-kebab-case';
import QRCode from 'qrcode' import QRCode from 'qrcode';
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server';
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate } from 'sveltekit-superforms/server' import { setError, superValidate } from 'sveltekit-superforms/server';
import type { PageServerLoad } from '../../$types' import type { PageServerLoad } from '../../$types';
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas' import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema)) const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema)) const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
// const addAuthNFactorForm = await superValidate(event, zod(addAuthNFactorSchema)); // const addAuthNFactorForm = await superValidate(event, zod(addAuthNFactorSchema));
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse) const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
if (error || !data) { if (error || !data) {
return fail(500, { return fail(500, {
addTwoFactorForm, addTwoFactorForm,
}) });
} }
const { totpCredential } = data const { totpCredential } = data;
if (totpCredential && authedUser.mfa_enabled) { if (totpCredential && authedUser.mfa_enabled) {
return { return {
addTwoFactorForm, addTwoFactorForm,
@ -38,41 +38,41 @@ export const load: PageServerLoad = async (event) => {
recoveryCodes: [], recoveryCodes: [],
totpUri: '', totpUri: '',
qrCode: '', qrCode: '',
} };
} }
if (totpCredential && !authedUser.mfa_enabled) { if (totpCredential && !authedUser.mfa_enabled) {
await locals.api.mfa.totp.$delete().then(locals.parseApiResponse) await locals.api.mfa.totp.$delete().then(locals.parseApiResponse);
} }
const issuer = kebabCase(env.PUBLIC_SITE_NAME) const issuer = kebabCase(env.PUBLIC_SITE_NAME);
const accountName = authedUser.email || authedUser.username const accountName = authedUser.email || authedUser.username;
const { data: createdTotpData, error: createdTotpError } = await locals.api.mfa.totp.$post().then(locals.parseApiResponse) const { data: createdTotpData, error: createdTotpError } = await locals.api.mfa.totp.$post().then(locals.parseApiResponse);
if (createdTotpError || !createdTotpData) { if (createdTotpError || !createdTotpData) {
return fail(500, { return fail(500, {
addTwoFactorForm, addTwoFactorForm,
}) });
} }
const { totpCredential: createdTotpCredentials } = createdTotpData const { totpCredential: createdTotpCredentials } = createdTotpData;
// pass the website's name and the user identifier (e.g. email, username) // pass the website's name and the user identifier (e.g. email, username)
if (!createdTotpCredentials?.secret_data) { if (!createdTotpCredentials?.secret_data) {
return fail(500, { return fail(500, {
addTwoFactorForm, addTwoFactorForm,
}) });
} }
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data) const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data);
const secret = encodeBase32(decodedHexSecret) const secret = encodeBase32(decodedHexSecret);
const intervalInSeconds = 30 const intervalInSeconds = 30;
const digits = 6 const digits = 6;
const totpUri = createTOTPKeyURI(issuer, accountName, decodedHexSecret, intervalInSeconds, digits) const totpUri = createTOTPKeyURI(issuer, accountName, decodedHexSecret, intervalInSeconds, digits);
addTwoFactorForm.data = { addTwoFactorForm.data = {
password: '', password: '',
two_factor_code: '', two_factor_code: '',
} };
return { return {
addTwoFactorForm, addTwoFactorForm,
removeTwoFactorForm, removeTwoFactorForm,
@ -81,84 +81,84 @@ export const load: PageServerLoad = async (event) => {
totpUri, totpUri,
qrCode: await QRCode.toDataURL(totpUri), qrCode: await QRCode.toDataURL(totpUri),
secret, secret,
} };
} };
export const actions: Actions = { export const actions: Actions = {
enableTotp: async (event) => { enableTotp: async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema)) const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
if (!addTwoFactorForm.valid) { if (!addTwoFactorForm.valid) {
return fail(400, { return fail(400, {
addTwoFactorForm, addTwoFactorForm,
}) });
} }
const { error: verifyPasswordError } = await locals.api.me.verify.password const { error: verifyPasswordError } = await locals.api.me.verify.password
.$post({ .$post({
json: { password: addTwoFactorForm.data.password }, json: { password: addTwoFactorForm.data.password },
}) })
.then(locals.parseApiResponse) .then(locals.parseApiResponse);
if (verifyPasswordError) { if (verifyPasswordError) {
console.log(verifyPasswordError) console.log(verifyPasswordError);
return setError(addTwoFactorForm, 'password', 'Your password is incorrect') return setError(addTwoFactorForm, 'password', 'Your password is incorrect');
} }
if (addTwoFactorForm.data.two_factor_code === '') { if (addTwoFactorForm.data.two_factor_code === '') {
return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code') return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code');
} }
const twoFactorCode = addTwoFactorForm.data.two_factor_code const twoFactorCode = addTwoFactorForm.data.two_factor_code;
const { error: verifyTotpError } = await locals.api.mfa.totp.verify const { error: verifyTotpError } = await locals.api.mfa.totp.verify
.$post({ .$post({
json: { code: twoFactorCode }, json: { code: twoFactorCode },
}) })
.then(locals.parseApiResponse) .then(locals.parseApiResponse);
if (verifyTotpError) { if (verifyTotpError) {
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code') return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code');
} }
redirect(302, '/settings/security/mfa/recovery-codes') redirect(302, '/settings/security/mfa/recovery-codes');
}, },
disableTotp: async (event) => { disableTotp: async (event) => {
const { locals } = event const { locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event) throw redirect(302, '/login', notSignedInMessage, event);
} }
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema)) const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
if (!removeTwoFactorForm.valid) { if (!removeTwoFactorForm.valid) {
return fail(400, { return fail(400, {
removeTwoFactorForm, removeTwoFactorForm,
}) });
} }
const { error: verifyPasswordError } = await locals.api.me.verify.password const { error: verifyPasswordError } = await locals.api.me.verify.password
.$post({ .$post({
json: { password: removeTwoFactorForm.data.password }, json: { password: removeTwoFactorForm.data.password },
}) })
.then(locals.parseApiResponse) .then(locals.parseApiResponse);
if (verifyPasswordError) { if (verifyPasswordError) {
console.log(verifyPasswordError) console.log(verifyPasswordError);
return setError(removeTwoFactorForm, 'password', 'Your password is incorrect') return setError(removeTwoFactorForm, 'password', 'Your password is incorrect');
} }
const { error: deleteTotpError } = await locals.api.mfa.totp.$delete().then(locals.parseApiResponse) const { error: deleteTotpError } = await locals.api.mfa.totp.$delete().then(locals.parseApiResponse);
if (deleteTotpError) { if (deleteTotpError) {
return fail(500, { return fail(500, {
removeTwoFactorForm, removeTwoFactorForm,
}) });
} }
redirect( redirect(
@ -169,6 +169,6 @@ export const actions: Actions = {
message: 'Two-Factor Authentication has been disabled.', message: 'Two-Factor Authentication has been disabled.',
}, },
event, event,
) );
}, },
} };

View file

@ -1,36 +1,34 @@
<script lang="ts"> <script lang="ts">
import CopyCodeBlock from '$components/CopyCodeBlock.svelte' import CopyCodeBlock from '$components/CopyCodeBlock.svelte';
import PinInput from '$components/pin-input.svelte' import PinInput from '$components/pin-input.svelte';
import * as Alert from '$components/ui/alert' import * as Form from '$components/ui/form';
import * as Form from '$components/ui/form' import { Input } from '$components/ui/input';
import { Input } from '$components/ui/input' import { zodClient } from 'sveltekit-superforms/adapters';
import { AlertTriangle } from 'lucide-svelte' import { superForm } from 'sveltekit-superforms/client';
import { zodClient } from 'sveltekit-superforms/adapters' import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas';
import { superForm } from 'sveltekit-superforms/client'
import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas'
const { data } = $props() const { data } = $props();
const { qrCode, secret, twoFactorEnabled, recoveryCodes } = data const { qrCode, secret, twoFactorEnabled, recoveryCodes } = data;
const addTwoFactorForm = superForm(data.addTwoFactorForm, { const addTwoFactorForm = superForm(data.addTwoFactorForm, {
taintedMessage: null, taintedMessage: null,
validators: zodClient(addTwoFactorSchema), validators: zodClient(addTwoFactorSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
}) });
const removeTwoFactorForm = superForm(data.removeTwoFactorForm, { const removeTwoFactorForm = superForm(data.removeTwoFactorForm, {
taintedMessage: null, taintedMessage: null,
validators: zodClient(removeTwoFactorSchema), validators: zodClient(removeTwoFactorSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
}) });
console.log('Two Factor: ', twoFactorEnabled, recoveryCodes) console.log('Two Factor: ', twoFactorEnabled, recoveryCodes);
const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm;
const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm;
</script> </script>
<section class="two-factor"> <section class="two-factor">

View file

@ -1,14 +1,14 @@
import { z } from 'zod' import { z } from 'zod';
export const addTwoFactorSchema = z.object({ export const addTwoFactorSchema = z.object({
password: z.string({ required_error: 'Current Password is required' }), password: z.string({ required_error: 'Current Password is required' }),
two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(), two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(),
}) });
export type AddTwoFactorSchema = typeof addTwoFactorSchema export type AddTwoFactorSchema = typeof addTwoFactorSchema;
export const removeTwoFactorSchema = addTwoFactorSchema.pick({ export const removeTwoFactorSchema = addTwoFactorSchema.pick({
password: true, password: true,
}) });
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema;

View file

@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$components/ui/card' import * as Card from '$components/ui/card';
const { data } = $props();
const { data } = $props() const { wishlists = [] } = data;
const { wishlists = [] } = data
</script> </script>
<svelte:head> <svelte:head>

View file

@ -1,8 +1,7 @@
import { notSignedInMessage } from '$lib/flashMessages.js'; import { notSignedInMessage } from '$lib/flashMessages.js';
import { db } from '$lib/server/api/packages/drizzle'; import { db } from '$lib/server/api/packages/drizzle';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import { modifyListGameSchema } from '$lib/validations/zod-schemas'; import { modifyListGameSchema } from '$lib/validations/zod-schemas';
import { type Actions, error, fail } from '@sveltejs/kit'; import { type Actions, error } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import { redirect } from 'sveltekit-flash-message/server'; import { redirect } from 'sveltekit-flash-message/server';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Game from '$components/Game.svelte' import Game from '$components/Game.svelte';
const { data } = $props() const { data } = $props();
const { items = [] } = data const { items = [] } = data;
</script> </script>
<svelte:head> <svelte:head>

View file

@ -1,12 +1,12 @@
import { loadFlash } from 'sveltekit-flash-message/server' import { loadFlash } from 'sveltekit-flash-message/server';
import type { LayoutServerLoad } from '../$types' import type { LayoutServerLoad } from '../$types';
export const load: LayoutServerLoad = loadFlash(async (event) => { export const load: LayoutServerLoad = loadFlash(async (event) => {
const { url, locals } = event const { url, locals } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
return { return {
url: url.pathname, url: url.pathname,
authedUser, authedUser,
} };
}) });

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import 'iconify-icon' import 'iconify-icon';
import Footer from '$components/Footer.svelte' import Footer from '$components/Footer.svelte';
import Header from '$components/Header.svelte' import Header from '$components/Header.svelte';
const { data, children } = $props() const { data, children } = $props();
</script> </script>
<div class="flex min-h-screen w-full flex-col"> <div class="flex min-h-screen w-full flex-col">

View file

@ -1,17 +1,17 @@
import { fail } from '@sveltejs/kit' import { fail } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags' import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals, url } = event const { locals, url } = event;
const authedUser = await locals.getAuthedUser() const authedUser = await locals.getAuthedUser();
const image = { const image = {
url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`, url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
width: 1200, width: 1200,
height: 630, height: 630,
} };
const metaTags: MetaTagsProps = Object.freeze({ const metaTags: MetaTagsProps = Object.freeze({
title: 'Home', title: 'Home',
description: 'Home page', description: 'Home page',
@ -33,18 +33,18 @@ export const load: PageServerLoad = async (event) => {
image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`, image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
imageAlt: 'Home | Bored Game', imageAlt: 'Home | Bored Game',
}, },
}) });
if (authedUser) { if (authedUser) {
const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse) const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse);
const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse) const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse);
if (wishlistsError || collectionsError) { if (wishlistsError || collectionsError) {
return fail(500, 'Failed to fetch wishlistsTable or collections') return fail(500, 'Failed to fetch wishlistsTable or collections');
} }
console.log('Wishlists', wishlistsData.wishlists) console.log('Wishlists', wishlistsData.wishlists);
console.log('Collections', collectionsData.collections) console.log('Collections', collectionsData.collections);
return { return {
metaTagsChild: metaTags, metaTagsChild: metaTags,
user: { user: {
@ -54,10 +54,10 @@ export const load: PageServerLoad = async (event) => {
}, },
wishlists: wishlistsData.wishlists, wishlists: wishlistsData.wishlists,
collections: collectionsData.collections, collections: collectionsData.collections,
} };
} }
console.log('Not Authed') console.log('Not Authed');
return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] } return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] };
} };

View file

@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import AddToList from '$components/AddToList.svelte' import AddToList from '$components/AddToList.svelte';
import Badge from '$components/ui/badge/badge.svelte' import Badge from '$components/ui/badge/badge.svelte';
import { Button } from '$components/ui/button' import { Button } from '$components/ui/button';
import { Dices, ExternalLinkIcon, MinusIcon, PlusIcon } from 'lucide-svelte' import { Dices, ExternalLinkIcon, MinusIcon, PlusIcon } from 'lucide-svelte';
import { Image } from 'svelte-lazy-loader' import { Image } from 'svelte-lazy-loader';
import type { PageData } from './$types'
const { data } = $props() const { data } = $props();
const { game, user, in_collection, in_wishlist } = data const { game, user, in_collection, in_wishlist } = data;
let seeMore: boolean = $state(false) let seeMore: boolean = $state(false);
</script> </script>
<svelte:head> <svelte:head>

View file

@ -1,65 +1,65 @@
import type { GameType, SearchQuery } from '$lib/types' import type { GameType, SearchQuery } from '$lib/types';
import { createOrUpdateGameMinimal } from '$lib/utils/db/gameUtils' import { createOrUpdateGameMinimal } from '$lib/utils/db/gameUtils';
import { mapAPIGameToBoredGame } from '$lib/utils/gameMapper.js' import { mapAPIGameToBoredGame } from '$lib/utils/gameMapper.js';
import { search_schema } from '$lib/zodValidation' import { search_schema } from '$lib/zodValidation';
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit';
import type { BggThingDto } from 'boardgamegeekclient/dist/esm/dto/index.js' import type { BggThingDto } from 'boardgamegeekclient/dist/esm/dto/index.js';
import kebabCase from 'just-kebab-case' import kebabCase from 'just-kebab-case';
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms/server' import { superValidate } from 'sveltekit-superforms/server';
async function searchForGames(locals: App.Locals, eventFetch: typeof fetch, urlQueryParams: URLSearchParams) { async function searchForGames(locals: App.Locals, eventFetch: typeof fetch, urlQueryParams: URLSearchParams) {
try { try {
console.log('urlQueryParams search games', urlQueryParams) console.log('urlQueryParams search games', urlQueryParams);
const headers = new Headers() const headers = new Headers();
headers.set('Content-Type', 'application/json') headers.set('Content-Type', 'application/json');
const requestInit: RequestInit = { const requestInit: RequestInit = {
method: 'GET', method: 'GET',
headers, headers,
} };
const url = `/api/games/search${urlQueryParams ? `?${urlQueryParams}` : ''}` const url = `/api/games/search${urlQueryParams ? `?${urlQueryParams}` : ''}`;
console.log('Calling internal api', url) console.log('Calling internal api', url);
const response = await eventFetch(url, requestInit) const response = await eventFetch(url, requestInit);
console.log('response from internal api', response) console.log('response from internal api', response);
if (response.status !== 404 && !response.ok) { if (response.status !== 404 && !response.ok) {
console.log('Status from internal api not 200', response.status) console.log('Status from internal api not 200', response.status);
error(response.status) error(response.status);
} }
const games = await response.json() const games = await response.json();
console.log('games from DB', games) console.log('games from DB', games);
const gameNameSearch = urlQueryParams.get('q') ?? '' const gameNameSearch = urlQueryParams.get('q') ?? '';
let totalCount = games?.length || 0 let totalCount = games?.length || 0;
if (totalCount === 0 || !games.find((game: GameType) => game.slug === kebabCase(gameNameSearch))) { if (totalCount === 0 || !games.find((game: GameType) => game.slug === kebabCase(gameNameSearch))) {
console.log('No games found in DB for', gameNameSearch) console.log('No games found in DB for', gameNameSearch);
const searchQueryParams = urlQueryParams ? `?${urlQueryParams}` : '' const searchQueryParams = urlQueryParams ? `?${urlQueryParams}` : '';
const externalResponse = await eventFetch(`/api/external/search${searchQueryParams}`, requestInit) const externalResponse = await eventFetch(`/api/external/search${searchQueryParams}`, requestInit);
console.log('Back from external search', externalResponse) console.log('Back from external search', externalResponse);
if (!externalResponse.ok) { if (!externalResponse.ok) {
console.log('Status not 200', externalResponse.status) console.log('Status not 200', externalResponse.status);
error(externalResponse.status) error(externalResponse.status);
} }
if (externalResponse.ok) { if (externalResponse.ok) {
const gameResponse = await externalResponse.json() const gameResponse = await externalResponse.json();
console.log('response from external api', gameResponse) console.log('response from external api', gameResponse);
const gameList: BggThingDto[] = gameResponse?.games const gameList: BggThingDto[] = gameResponse?.games;
totalCount = gameResponse?.totalCount totalCount = gameResponse?.totalCount;
console.log('totalCount', totalCount) console.log('totalCount', totalCount);
for (const game of gameList) { for (const game of gameList) {
console.log(`Retrieving simplified external game details for id: ${game.id} with name ${game.name}`) console.log(`Retrieving simplified external game details for id: ${game.id} with name ${game.name}`);
const externalGameResponse = await eventFetch(`/api/external/game/${game.id}?simplified=true`) const externalGameResponse = await eventFetch(`/api/external/game/${game.id}?simplified=true`);
if (externalGameResponse.ok) { if (externalGameResponse.ok) {
const externalGame = await externalGameResponse.json() const externalGame = await externalGameResponse.json();
console.log('externalGame', externalGame) console.log('externalGame', externalGame);
const boredGame = mapAPIGameToBoredGame(externalGame) const boredGame = mapAPIGameToBoredGame(externalGame);
games.push(createOrUpdateGameMinimal(locals, boredGame, externalGame.id)) games.push(createOrUpdateGameMinimal(locals, boredGame, externalGame.id));
} }
} }
} }
@ -68,14 +68,14 @@ async function searchForGames(locals: App.Locals, eventFetch: typeof fetch, urlQ
return { return {
totalCount, totalCount,
games, games,
} };
} catch (e) { } catch (e) {
console.log(`Error searching board games ${e}`) console.log(`Error searching board games ${e}`);
} }
return { return {
totalCount: 0, totalCount: 0,
games: [], games: [],
} };
} }
const defaults = { const defaults = {
@ -85,14 +85,14 @@ const defaults = {
sort: 'asc', sort: 'asc',
q: '', q: '',
exact: false, exact: false,
} };
export const load = async ({ locals, fetch, url }) => { export const load = async ({ locals, fetch, url }) => {
const searchParams = Object.fromEntries(url?.searchParams) const searchParams = Object.fromEntries(url?.searchParams);
console.log('searchParams', searchParams) console.log('searchParams', searchParams);
searchParams.order = searchParams.order || defaults.order searchParams.order = searchParams.order || defaults.order;
searchParams.sort = searchParams.sort || defaults.sort searchParams.sort = searchParams.sort || defaults.sort;
searchParams.q = searchParams.q || defaults.q searchParams.q = searchParams.q || defaults.q;
const form = await superValidate( const form = await superValidate(
{ {
...searchParams, ...searchParams,
@ -101,14 +101,14 @@ export const load = async ({ locals, fetch, url }) => {
exact: searchParams.exact ? searchParams.exact === 'true' : defaults.exact, exact: searchParams.exact ? searchParams.exact === 'true' : defaults.exact,
}, },
zod(search_schema), zod(search_schema),
) );
const queryParams: SearchQuery = { const queryParams: SearchQuery = {
limit: form.data?.limit, limit: form.data?.limit,
skip: form.data?.skip, skip: form.data?.skip,
q: form.data?.q, q: form.data?.q,
exact: form.data?.exact, exact: form.data?.exact,
} };
try { try {
if (form.data?.q === '') { if (form.data?.q === '') {
@ -119,53 +119,53 @@ export const load = async ({ locals, fetch, url }) => {
games: [], games: [],
wishlists: [], wishlists: [],
}, },
} };
} }
if (form.data?.minAge) { if (form.data?.minAge) {
if (form.data?.exactMinAge) { if (form.data?.exactMinAge) {
queryParams.min_age = form.data?.minAge queryParams.min_age = form.data?.minAge;
} else { } else {
queryParams.gt_min_age = form.data?.minAge === 1 ? 0 : form.data?.minAge - 1 queryParams.gt_min_age = form.data?.minAge === 1 ? 0 : form.data?.minAge - 1;
} }
} }
if (form.data?.minPlayers) { if (form.data?.minPlayers) {
if (form.data?.exactMinPlayers) { if (form.data?.exactMinPlayers) {
queryParams.min_players = form.data?.minPlayers queryParams.min_players = form.data?.minPlayers;
} else { } else {
queryParams.gt_min_players = form.data?.minPlayers === 1 ? 0 : form.data?.minPlayers - 1 queryParams.gt_min_players = form.data?.minPlayers === 1 ? 0 : form.data?.minPlayers - 1;
} }
} }
if (form.data?.maxPlayers) { if (form.data?.maxPlayers) {
if (form.data?.exactMaxPlayers) { if (form.data?.exactMaxPlayers) {
queryParams.max_players = form.data?.maxPlayers queryParams.max_players = form.data?.maxPlayers;
} else { } else {
queryParams.lt_max_players = form.data?.maxPlayers + 1 queryParams.lt_max_players = form.data?.maxPlayers + 1;
} }
} }
const newQueryParams: Record<string, string> = {} const newQueryParams: Record<string, string> = {};
for (const key in queryParams) { for (const key in queryParams) {
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}` newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
} }
const urlQueryParams = new URLSearchParams(newQueryParams) const urlQueryParams = new URLSearchParams(newQueryParams);
const searchData = await searchForGames(locals, fetch, urlQueryParams) const searchData = await searchForGames(locals, fetch, urlQueryParams);
console.log('search data', JSON.stringify(searchData, null, 2)) console.log('search data', JSON.stringify(searchData, null, 2));
return { return {
form, form,
// modifyListForm, // modifyListForm,
searchData, searchData,
wishlists: [], wishlists: [],
} };
} catch (e) { } catch (e) {
console.log(`Error searching board games ${e}`) console.log(`Error searching board games ${e}`);
} }
console.log('returning default no data') console.log('returning default no data');
return { return {
form, form,
searchData: { searchData: {
@ -173,29 +173,29 @@ export const load = async ({ locals, fetch, url }) => {
games: [], games: [],
}, },
wishlists: [], wishlists: [],
} };
} };
export const actions = { export const actions = {
random: async ({ request, locals, fetch }) => { random: async ({ request, locals, fetch }) => {
const form = await superValidate(request, zod(search_schema)) const form = await superValidate(request, zod(search_schema));
const queryParams: SearchQuery = { const queryParams: SearchQuery = {
order_by: 'rank', order_by: 'rank',
ascending: false, ascending: false,
random: true, random: true,
fields: 'id,name,min_age,min_players,max_players,thumb_url,min_playtime,max_playtime,min_age,description', fields: 'id,name,min_age,min_players,max_players,thumb_url,min_playtime,max_playtime,min_age,description',
} };
const newQueryParams: Record<string, string> = {} const newQueryParams: Record<string, string> = {};
for (const key in queryParams) { for (const key in queryParams) {
newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}` newQueryParams[key] = `${queryParams[key as keyof SearchQuery]}`;
} }
const urlQueryParams = new URLSearchParams(newQueryParams) const urlQueryParams = new URLSearchParams(newQueryParams);
return { return {
form, form,
searchData: await searchForGames(locals, fetch, urlQueryParams), searchData: await searchForGames(locals, fetch, urlQueryParams),
} };
}, },
} };

Some files were not shown because too many files have changed in this diff Show more