mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
commit
6836ebe64c
43 changed files with 835 additions and 836 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
# Private
|
# Private
|
||||||
ORIGIN=http://localhost:5173
|
DOMAIN=localhost
|
||||||
|
ORIGIN=http://$DOMAIN:5173
|
||||||
|
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ GOOGLE_CLIENT_SECRET=""
|
||||||
# Public
|
# Public
|
||||||
|
|
||||||
PUBLIC_SITE_NAME='Bored Game'
|
PUBLIC_SITE_NAME='Bored Game'
|
||||||
PUBLIC_SITE_URL='http://localhost:5173'
|
PUBLIC_SITE_URL='http://$DOMAIN:5173'
|
||||||
PUBLIC_UMAMI_DO_NOT_TRACK=true
|
PUBLIC_UMAMI_DO_NOT_TRACK=true
|
||||||
PUBLIC_UMAMI_URL=
|
PUBLIC_UMAMI_URL=
|
||||||
PUBLIC_UMAMI_ID=
|
PUBLIC_UMAMI_ID=
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
|
container_name: boredgame_postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|
@ -11,6 +13,7 @@ services:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
|
container_name: boredgame_redis
|
||||||
ports:
|
ports:
|
||||||
- '6379:6379'
|
- '6379:6379'
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -23,18 +26,6 @@ services:
|
||||||
# - '3592:3592'
|
# - '3592:3592'
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./policies:/policies
|
# - ./policies:/policies
|
||||||
# caddy:
|
|
||||||
# image: caddy:latest
|
|
||||||
# restart: unless-stopped
|
|
||||||
# ports:
|
|
||||||
# - "80:80"
|
|
||||||
# - "443:443"
|
|
||||||
# - "443:443/udp"
|
|
||||||
# volumes:
|
|
||||||
# - ./Caddyfile:/etc/caddy/Caddyfile
|
|
||||||
# - ./site:/srv
|
|
||||||
# - caddy_data:/data
|
|
||||||
# - caddy_config:/config
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
"@hono/zod-validator": "^0.2.2",
|
"@hono/zod-validator": "^0.2.2",
|
||||||
"@iconify-icons/line-md": "^1.2.30",
|
"@iconify-icons/line-md": "^1.2.30",
|
||||||
"@iconify-icons/mdi": "^1.2.48",
|
"@iconify-icons/mdi": "^1.2.48",
|
||||||
"@internationalized/date": "^3.5.5",
|
"@internationalized/date": "^3.5.6",
|
||||||
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
||||||
"@lukeed/uuid": "^2.0.1",
|
"@lukeed/uuid": "^2.0.1",
|
||||||
"@neondatabase/serverless": "^0.9.5",
|
"@neondatabase/serverless": "^0.9.5",
|
||||||
|
|
@ -95,9 +95,9 @@
|
||||||
"@sveltejs/adapter-node": "^5.2.5",
|
"@sveltejs/adapter-node": "^5.2.5",
|
||||||
"@sveltejs/adapter-vercel": "^5.4.4",
|
"@sveltejs/adapter-vercel": "^5.4.4",
|
||||||
"@types/feather-icons": "^4.29.4",
|
"@types/feather-icons": "^4.29.4",
|
||||||
"bits-ui": "^0.21.13",
|
"bits-ui": "^0.21.16",
|
||||||
"boardgamegeekclient": "^1.9.1",
|
"boardgamegeekclient": "^1.9.1",
|
||||||
"bullmq": "^5.14.0",
|
"bullmq": "^5.15.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
|
|
@ -116,6 +116,7 @@
|
||||||
"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.6",
|
"open-props": "^1.7.6",
|
||||||
"oslo": "^1.2.1",
|
"oslo": "^1.2.1",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
|
|
@ -124,8 +125,8 @@
|
||||||
"radix-svelte": "^0.9.0",
|
"radix-svelte": "^0.9.0",
|
||||||
"rate-limit-redis": "^4.2.0",
|
"rate-limit-redis": "^4.2.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"svelte-french-toast": "^1.2.0",
|
|
||||||
"svelte-lazy-loader": "^1.0.0",
|
"svelte-lazy-loader": "^1.0.0",
|
||||||
|
"svelte-sonner": "^0.3.28",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwind-variants": "^0.2.1",
|
"tailwind-variants": "^0.2.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
|
||||||
1134
pnpm-lock.yaml
1134
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
17
src/app.d.ts
vendored
17
src/app.d.ts
vendored
|
|
@ -21,17 +21,12 @@ declare global {
|
||||||
parseApiResponse: typeof parseApiResponse;
|
parseApiResponse: typeof parseApiResponse;
|
||||||
getAuthedUser: () => Promise<Returned<User> | null>;
|
getAuthedUser: () => Promise<Returned<User> | null>;
|
||||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||||
auth: import('lucia').AuthRequest;
|
}
|
||||||
user: import('lucia').User | null;
|
namespace Superforms {
|
||||||
session: import('lucia').Session | null;
|
type Message = {
|
||||||
startTimer: number;
|
type: 'error' | 'success' | 'info',
|
||||||
ip: string;
|
text: string
|
||||||
country: string;
|
}
|
||||||
error: string;
|
|
||||||
errorId: string;
|
|
||||||
errorStackTrace: string;
|
|
||||||
message: unknown;
|
|
||||||
track: unknown;
|
|
||||||
}
|
}
|
||||||
interface Error {
|
interface Error {
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|
|
||||||
29
src/app.html
29
src/app.html
|
|
@ -6,36 +6,7 @@
|
||||||
<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" />
|
||||||
<script>
|
|
||||||
// const htmlElement = document.documentElement;
|
|
||||||
// const userTheme = localStorage.theme;
|
|
||||||
// const userFont = localStorage.font;
|
|
||||||
|
|
||||||
// const prefersDarkMode = window.matchMedia('prefers-color-scheme: dark').matches;
|
|
||||||
// const prefersLightMode = window.matchMedia('prefers-color-scheme: light').matches;
|
|
||||||
|
|
||||||
// // check if the user set a theme
|
|
||||||
// if (userTheme) {
|
|
||||||
// htmlElement.dataset.theme = userTheme;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // otherwise check for user preference
|
|
||||||
// if (!userTheme && prefersDarkMode) {
|
|
||||||
// htmlElement.dataset.theme = '🌛 Night';
|
|
||||||
// localStorage.theme = '🌛 Night';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!userTheme && prefersLightMode) {
|
|
||||||
// htmlElement.dataset.theme = '☀️ Daylight';
|
|
||||||
// localStorage.theme = '☀️ Daylight';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // if nothing is set default to dark mode
|
|
||||||
// if (!userTheme && !prefersDarkMode && !prefersLightMode) {
|
|
||||||
// htmlElement.dataset.theme = '🌛 Night';
|
|
||||||
// localStorage.theme = '🌛 Night';
|
|
||||||
// }
|
|
||||||
</script>
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { applyAction, enhance } from '$app/forms'
|
import { applyAction, enhance } from '$app/forms';
|
||||||
import { invalidateAll } from '$app/navigation'
|
import { invalidateAll } from '$app/navigation';
|
||||||
import Logo from '$components/logo.svelte'
|
import Logo from '$components/logo.svelte';
|
||||||
import * as Avatar from '$components/ui/avatar'
|
import * as Avatar from '$components/ui/avatar';
|
||||||
import * as DropdownMenu from '$components/ui/dropdown-menu'
|
import * as DropdownMenu from '$components/ui/dropdown-menu';
|
||||||
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte'
|
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte';
|
||||||
import toast from 'svelte-french-toast'
|
|
||||||
|
|
||||||
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>
|
||||||
|
|
@ -63,26 +62,7 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</a>
|
</a>
|
||||||
<DropdownMenu.Item>
|
<DropdownMenu.Item>
|
||||||
<form
|
<form action="/logout" method="POST">
|
||||||
use:enhance={() => {
|
|
||||||
return async ({ result }) => {
|
|
||||||
console.log(result);
|
|
||||||
if (result.type === 'success' || result.type === 'redirect') {
|
|
||||||
toast.success('Logged Out');
|
|
||||||
} else if (result.type === 'error') {
|
|
||||||
console.log(result);
|
|
||||||
toast.error(`Error: ${result.error.message}`);
|
|
||||||
} else {
|
|
||||||
toast.error(`Something went wrong.`);
|
|
||||||
console.log(result);
|
|
||||||
}
|
|
||||||
await invalidateAll();
|
|
||||||
await applyAction(result);
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
action="/logout"
|
|
||||||
method="POST"
|
|
||||||
>
|
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<LogOut class="mr-2 h-4 w-4" />
|
<LogOut class="mr-2 h-4 w-4" />
|
||||||
|
|
|
||||||
13
src/lib/components/PlausibleAnalytics.svelte
Normal file
13
src/lib/components/PlausibleAnalytics.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { PUBLIC_SITE_URL } from '$env/static/public'
|
||||||
|
const src = `${PUBLIC_SITE_URL}/js/script.js`
|
||||||
|
const dataDomain = PUBLIC_SITE_URL.replace('https://', '').replace('http://', '')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
data-domain={dataDomain}
|
||||||
|
{src}
|
||||||
|
></script>
|
||||||
|
</svelte:head>
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type SvelteComponent, createEventDispatcher } from 'svelte';
|
// import {
|
||||||
import { fade } from 'svelte/transition';
|
// Dialog,
|
||||||
// import {
|
// DialogDescription,
|
||||||
// Dialog,
|
// DialogOverlay,
|
||||||
// DialogDescription,
|
// DialogTitle
|
||||||
// DialogOverlay,
|
// } from '@rgossiaux/svelte-headlessui';
|
||||||
// DialogTitle
|
import { boredState } from '$lib/stores/boredState'
|
||||||
// } from '@rgossiaux/svelte-headlessui';
|
import { type SvelteComponent, createEventDispatcher } from 'svelte'
|
||||||
import { boredState } from '$lib/stores/boredState';
|
import { fade } from 'svelte/transition'
|
||||||
|
|
||||||
export let title: string;
|
export let title: string
|
||||||
export let description: string;
|
export let description: string
|
||||||
export let danger = false;
|
export let danger = false
|
||||||
export let alert = false;
|
export let alert = false
|
||||||
export let passive = false;
|
export let passive = false
|
||||||
export let primaryButtonText = '';
|
export let primaryButtonText = ''
|
||||||
export let primaryButtonDisabled = false;
|
export let primaryButtonDisabled = false
|
||||||
export let primaryButtonIcon: typeof SvelteComponent<any> = undefined;
|
export let primaryButtonIcon: typeof SvelteComponent<any> = undefined
|
||||||
export let primaryButtonIconDescription = '';
|
export let primaryButtonIconDescription = ''
|
||||||
export let secondaryButtonText = '';
|
export let secondaryButtonText = ''
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: isOpen = $boredState?.dialog?.isOpen;
|
$: isOpen = $boredState?.dialog?.isOpen
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <Dialog
|
<!-- <Dialog
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- </Dialog> -->
|
<!-- </Dialog> -->
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="postcss">
|
||||||
.dialog {
|
.dialog {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("p-6 pt-0", className)} {...$$restProps}>
|
<div class={cn("p-6", className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLParagraphElement>;
|
type $$Props = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class={cn("text-sm text-muted-foreground", className)} {...$$restProps}>
|
<p class={cn("text-muted-foreground text-sm", className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("flex flex-col space-y-1.5 p-6", className)} {...$$restProps}>
|
<div class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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.js";
|
import { cn } from "$lib/utils/ui.js";
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||||
tag?: HeadingLevel;
|
tag?: HeadingLevel;
|
||||||
|
|
|
||||||
|
|
@ -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<HTMLDivElement>;
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
||||||
1
src/lib/components/ui/sonner/index.ts
Normal file
1
src/lib/components/ui/sonner/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Toaster } from "./sonner.svelte";
|
||||||
20
src/lib/components/ui/sonner/sonner.svelte
Normal file
20
src/lib/components/ui/sonner/sonner.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||||
|
import { mode } from "mode-watcher";
|
||||||
|
|
||||||
|
type $$Props = SonnerProps;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sonner
|
||||||
|
theme={$mode}
|
||||||
|
class="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classes: {
|
||||||
|
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
|
|
@ -1,23 +1,9 @@
|
||||||
import env from './env'
|
import env from './env'
|
||||||
import type { Config } from './types/config'
|
import type { Config } from './types/config'
|
||||||
|
|
||||||
const isPreview = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development'
|
|
||||||
|
|
||||||
let domain: string
|
|
||||||
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
|
|
||||||
domain = 'boredgame.vercel.app'
|
|
||||||
} else if (isPreview && process.env.VERCEL_BRANCH_URL !== undefined) {
|
|
||||||
domain = process.env.VERCEL_BRANCH_URL
|
|
||||||
} else {
|
|
||||||
domain = 'localhost'
|
|
||||||
}
|
|
||||||
|
|
||||||
// export const config = { ...env, isProduction: process.env.NODE_ENV === 'production'
|
|
||||||
// || process.env.VERCEL_ENV === 'production', domain };
|
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
isProduction: process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production',
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
domain,
|
domain: env.DOMAIN,
|
||||||
api: {
|
api: {
|
||||||
origin: env.ORIGIN,
|
origin: env.ORIGIN,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const EnvSchema = z.object({
|
||||||
DATABASE_DB: z.string(),
|
DATABASE_DB: z.string(),
|
||||||
DB_MIGRATING: stringBoolean,
|
DB_MIGRATING: stringBoolean,
|
||||||
DB_SEEDING: stringBoolean,
|
DB_SEEDING: stringBoolean,
|
||||||
|
DOMAIN: z.string(),
|
||||||
GITHUB_CLIENT_ID: z.string(),
|
GITHUB_CLIENT_ID: z.string(),
|
||||||
GITHUB_CLIENT_SECRET: z.string(),
|
GITHUB_CLIENT_SECRET: z.string(),
|
||||||
GOOGLE_CLIENT_ID: z.string(),
|
GOOGLE_CLIENT_ID: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,6 @@ container.resolve(AuthCleanupJobs).deleteStaleLoginRequests()
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Exports */
|
/* Exports */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
export const rpc = hc<typeof routes>(config.ORIGIN)
|
export const rpc = hc<typeof routes>(config.api.origin)
|
||||||
export type ApiClient = typeof rpc
|
export type ApiClient = typeof rpc
|
||||||
export type ApiRoutes = typeof routes
|
export type ApiRoutes = typeof routes
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@ export class CollectionsRepository {
|
||||||
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
||||||
return db.query.collections.findMany({
|
return db.query.collections.findMany({
|
||||||
where: eq(collections.user_id, userId),
|
where: eq(collections.user_id, userId),
|
||||||
|
columns: {
|
||||||
|
cuid: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export class RecoveryCodesRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
async findAllByUserId(userId: string, db = this.drizzle.db) {
|
||||||
return db.query.recoveryCodesTable.findFirst({
|
return db.query.recoveryCodesTable.findMany({
|
||||||
where: eq(recoveryCodesTable.userId, userId),
|
where: eq(recoveryCodesTable.userId, userId),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export class WishlistsRepository {
|
||||||
columns: {
|
columns: {
|
||||||
cuid: true,
|
cuid: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository'
|
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository'
|
||||||
import { HMAC } from 'oslo/crypto'
|
|
||||||
import { decodeHex, encodeHexLowerCase } from '@oslojs/encoding'
|
import { decodeHex, encodeHexLowerCase } from '@oslojs/encoding'
|
||||||
import { verifyTOTP } from '@oslojs/otp'
|
import { verifyTOTP } from '@oslojs/otp'
|
||||||
import { inject, injectable } from 'tsyringe'
|
import { inject, injectable } from 'tsyringe'
|
||||||
|
|
@ -22,12 +21,11 @@ export class TotpService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(userId: string) {
|
async create(userId: string) {
|
||||||
const twoFactorSecret = await new HMAC('SHA-1').generateKey()
|
const secret = new Uint8Array(20)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.credentialsRepository.create({
|
return await this.credentialsRepository.create({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
secret_data: encodeHexLowerCase(twoFactorSecret),
|
secret_data: encodeHexLowerCase(crypto.getRandomValues(secret)),
|
||||||
type: 'totp',
|
type: 'totp',
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
20
src/lib/utils/superforms.ts
Normal file
20
src/lib/utils/superforms.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { toast } from 'svelte-sonner'
|
||||||
|
import { message, type ErrorStatus, type SuperValidated } from 'sveltekit-superforms'
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
type: 'error' | 'success' | 'info'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorMessage(form: SuperValidated<any>, text: string | null, status: ErrorStatus = 500) {
|
||||||
|
return message(form, { text: text || 'Error', type: 'error' }, { status })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function successMessage(form: SuperValidated<any>, text: string | null) {
|
||||||
|
return message(form, { text: text || 'Success', type: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toastMessage(message: Message | undefined) {
|
||||||
|
if (!message) return
|
||||||
|
toast[message.type](message.text)
|
||||||
|
}
|
||||||
|
|
@ -23,15 +23,13 @@ export const flyAndScale = (node: Element, params: FlyAndScaleParams = { y: -8,
|
||||||
const [minB, maxB] = scaleB
|
const [minB, maxB] = scaleB
|
||||||
|
|
||||||
const percentage = (valueA - minA) / (maxA - minA)
|
const percentage = (valueA - minA) / (maxA - minA)
|
||||||
const valueB = percentage * (maxB - minB) + minB
|
return percentage * (maxB - minB) + minB
|
||||||
|
|
||||||
return valueB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
||||||
return Object.keys(style).reduce((str, key) => {
|
return Object.keys(style).reduce((str, key) => {
|
||||||
if (style[key] === undefined) return str
|
if (style[key] === undefined) return str
|
||||||
return str + `${key}:${style[key]};`
|
return `${str}${key}:${style[key]};`
|
||||||
}, '')
|
}, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages'
|
import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages'
|
||||||
import { user_roles } from '$lib/server/api/databases/tables'
|
import { user_roles } from '$lib/server/api/databases/tables'
|
||||||
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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { theme } from '$state/theme';
|
import { theme } from '$state/theme';
|
||||||
import toast, { Toaster } from 'svelte-french-toast';
|
import { toastMessage } from '$lib/utils/superforms.js';
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
const { user } = data;
|
const { user } = data;
|
||||||
|
|
@ -19,29 +19,13 @@
|
||||||
$theme = user?.theme || 'system';
|
$theme = user?.theme || 'system';
|
||||||
document.querySelector('html')?.setAttribute('data-theme', $theme);
|
document.querySelector('html')?.setAttribute('data-theme', $theme);
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if ($flash) {
|
|
||||||
if ($flash.type === 'success') {
|
|
||||||
toast.success($flash.message);
|
|
||||||
} else {
|
|
||||||
toast.error($flash.message, {
|
|
||||||
duration: 5000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clearing the flash message could sometimes
|
|
||||||
// be required here to avoid double-toasting.
|
|
||||||
flash.set(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Do the admin stuff</h1>
|
<h1>Do the admin stuff</h1>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
<Toaster />
|
<!-- <Toaster /> -->
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
:global(main) {
|
:global(main) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<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>
|
||||||
|
|
@ -9,14 +10,13 @@ let collections = data?.collections || []
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Your Collections</h1>
|
<h1>Your Collections</h1>
|
||||||
|
|
||||||
<div class="collection-list">
|
<div class="collection-list">
|
||||||
{#if collections.length === 0}
|
{#if collections.length === 0}
|
||||||
<h2>You have no collections</h2>
|
<h2>You have no collections</h2>
|
||||||
{:else}
|
{:else}
|
||||||
{#each collections as collection}
|
{#each collections as collection}
|
||||||
<Card.Root>
|
<Card.Root class="shadow-sm hover:shadow-md transition-shadow duration-300 ease-in-out">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{collection.name}</Card.Title>
|
<Card.Title>{collection.name}</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
|
@ -25,10 +25,6 @@ let collections = data?.collections || []
|
||||||
<p>Created at: {new Date(collection.createdAt).toLocaleString()}</p>
|
<p>Created at: {new Date(collection.createdAt).toLocaleString()}</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
<!-- <div class="collection grid gap-0.5">
|
|
||||||
<h2><a href="/collections/{collection.cuid}">{collection.name}</a></h2>
|
|
||||||
<h3>Created at: {new Date(collection.createdAt).toLocaleString()}</h3>
|
|
||||||
</div> -->
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,21 @@ let { children } = $props()
|
||||||
.security-nav {
|
.security-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@media (width <= 1000px) {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
width: 16rem;
|
@media (width > 1000px) {
|
||||||
position: sticky;
|
width: 16rem;
|
||||||
top: 0;
|
position: sticky;
|
||||||
left: 0;
|
top: 0;
|
||||||
background-color: #fff;
|
left: 0;
|
||||||
padding: 1rem;
|
background-color: #fff;
|
||||||
border-right: 1px solid #ddd;
|
padding: 1rem;
|
||||||
height: 100vh;
|
border-right: 1px solid #ddd;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
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 { 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 { encodeBase32, decodeHex } from '@oslojs/encoding'
|
|
||||||
import { createTOTPKeyURI } from '@oslojs/otp'
|
|
||||||
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'
|
||||||
|
|
@ -63,7 +63,7 @@ export const load: PageServerLoad = async (event) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data)
|
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data)
|
||||||
const secret = encodeBase32(new TextEncoder().encode(decodedHexSecret))
|
const secret = encodeBase32(decodedHexSecret)
|
||||||
const intervalInSeconds = 30
|
const intervalInSeconds = 30
|
||||||
const digits = 6
|
const digits = 6
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { notSignedInMessage } from '$lib/flashMessages.js'
|
import { notSignedInMessage } from '$lib/flashMessages.js'
|
||||||
import { gamesTable, wishlist_items, wishlistsTable } from '$lib/server/api/databases/tables'
|
import { gamesTable, wishlist_items, wishlistsTable } from '$lib/server/api/databases/tables'
|
||||||
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, fail } from '@sveltejs/kit'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
|
@ -17,15 +16,8 @@ export async function load(event) {
|
||||||
throw redirect(302, '/login', notSignedInMessage, event)
|
throw redirect(302, '/login', notSignedInMessage, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userWishlists = await db.query.wishlists.findMany({
|
const { data } = await locals.api.wishlists.$get().then(locals.parseApiResponse)
|
||||||
columns: {
|
const userWishlists = data?.wishlists
|
||||||
cuid: true,
|
|
||||||
name: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
where: eq(wishlistsTable.user_id, authedUser.id),
|
|
||||||
})
|
|
||||||
console.log('wishlists', userWishlists)
|
|
||||||
|
|
||||||
if (userWishlists?.length === 0) {
|
if (userWishlists?.length === 0) {
|
||||||
console.log('Wishlists not found')
|
console.log('Wishlists not found')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as Card from '$components/ui/card'
|
||||||
|
|
||||||
const { data } = $props()
|
const { data } = $props()
|
||||||
const { wishlists = [] } = data
|
const { wishlists = [] } = data
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -8,21 +10,25 @@ const { wishlists = [] } = data
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Your wishlistsTable</h1>
|
<h1>Your wishlists</h1>
|
||||||
|
|
||||||
<div class="wishlists">
|
<div class="wishlist-list">
|
||||||
<div class="wishlist-list">
|
{#if wishlists.length === 0}
|
||||||
{#if wishlists.length === 0}
|
<h2>You have no wishlists</h2>
|
||||||
<h2>You have no wishlistsTable</h2>
|
{:else}
|
||||||
{:else}
|
{#each wishlists as wishlist}
|
||||||
{#each wishlists as wishlist}
|
<Card.Root class="shadow-sm hover:shadow-md transition-shadow duration-300 ease-in-out">
|
||||||
<div class="collection grid gap-0.5">
|
<a href="/wishlists/{wishlist.cuid}">
|
||||||
<h2><a href="/wishlists/{wishlist.cuid}">{wishlist.name}</a></h2>
|
<Card.Header>
|
||||||
<h3>Created at: {new Date(wishlist.created_at).toLocaleString()}</h3>
|
<Card.Title>{wishlist.name}</Card.Title>
|
||||||
</div>
|
</Card.Header>
|
||||||
{/each}
|
<Card.Content>
|
||||||
{/if}
|
<h3>Created at: {new Date(wishlist.createdAt).toLocaleString()}</h3>
|
||||||
</div>
|
</Card.Content>
|
||||||
|
</a>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -32,10 +38,6 @@ const { wishlists = [] } = data
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wishlists {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wishlist-list {
|
.wishlist-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const { data, children } = $props()
|
||||||
<Header user={data.authedUser} />
|
<Header user={data.authedUser} />
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 p-4 md:gap-8 md:p-10"
|
class="flex min-h-[calc(100vh-theme(spacing.16))] flex-1 flex-col gap-4 p-4 md:gap-8 md:p-10"
|
||||||
>
|
>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -19,22 +19,6 @@ const { data, children } = $props()
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
/*main {*/
|
|
||||||
/* flex: 1;*/
|
|
||||||
/* display: flex;*/
|
|
||||||
/* flex-direction: column;*/
|
|
||||||
/* max-width: 850px;*/
|
|
||||||
/* margin: 0 auto;*/
|
|
||||||
/* padding: 2rem 0rem;*/
|
|
||||||
/* max-width: 80vw;*/
|
|
||||||
|
|
||||||
/* @media (min-width: 1600px) {*/
|
|
||||||
/* max-width: 70vw;*/
|
|
||||||
/* }*/
|
|
||||||
|
|
||||||
/* box-sizing: border-box;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
:global(.dialog-overlay) {
|
:global(.dialog-overlay) {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const load: PageServerLoad = async (event) => {
|
||||||
url: new URL(url.pathname, url.origin).href,
|
url: new URL(url.pathname, url.origin).href,
|
||||||
locale: 'en_US',
|
locale: 'en_US',
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
description: 'Bored Game, keep track of your gamesTable',
|
description: 'Bored Game, keep track of your games',
|
||||||
images: [image],
|
images: [image],
|
||||||
siteName: 'Bored Game',
|
siteName: 'Bored Game',
|
||||||
},
|
},
|
||||||
|
|
@ -29,7 +29,7 @@ export const load: PageServerLoad = async (event) => {
|
||||||
site: '@boredgame',
|
site: '@boredgame',
|
||||||
cardType: 'summary_large_image',
|
cardType: 'summary_large_image',
|
||||||
title: 'Home | Bored Game',
|
title: 'Home | Bored Game',
|
||||||
description: 'Bored Game, keep track of your gamesTable',
|
description: 'Bored Game, 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`,
|
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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const welcomeName = $derived.by(() => {
|
||||||
welcomeName += data?.user?.firstName
|
welcomeName += data?.user?.firstName
|
||||||
}
|
}
|
||||||
if (data?.user?.lastName) {
|
if (data?.user?.lastName) {
|
||||||
welcomeName += ' ' + data?.user?.lastName
|
welcomeName = welcomeName.length === 0 ? data?.user?.lastName : welcomeName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (welcomeName.length === 0) {
|
if (welcomeName.length === 0) {
|
||||||
|
|
@ -23,7 +23,7 @@ const welcomeName = $derived.by(() => {
|
||||||
{#if user}
|
{#if user}
|
||||||
<h1>Welcome, {welcomeName}!</h1>
|
<h1>Welcome, {welcomeName}!</h1>
|
||||||
<div>
|
<div>
|
||||||
<h2>You wishlistsTable:</h2>
|
<h2>You wishlists:</h2>
|
||||||
{#each wishlists as wishlist}
|
{#each wishlists as wishlist}
|
||||||
<a href="/wishlists/{wishlist.cuid}">{wishlist.name}</a>
|
<a href="/wishlists/{wishlist.cuid}">{wishlist.name}</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -36,7 +36,7 @@ const welcomeName = $derived.by(() => {
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<h1>Welcome to Bored Game!</h1>
|
<h1>Welcome to Bored Game!</h1>
|
||||||
<h2>Track the board gamesTable you own, the ones you want, and whether you play them enough.</h2>
|
<h2>Track the board games you own, the ones you want, and whether you play them enough.</h2>
|
||||||
<p>Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an account.</p>
|
<p>Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an account.</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>About Bored Game</h1>
|
<h1>About Bored Game</h1>
|
||||||
<article>
|
<article>
|
||||||
<p>One day we were bored and wanted to play one of our board gamesTable.</p>
|
<p>One day we were bored and wanted to play one of our board games.</p>
|
||||||
<p>Our problem was that we didn't know which one to play.</p>
|
<p>Our problem was that we didn't know which one to play.</p>
|
||||||
<p>Rather than just pick a game I decided to make this overcomplicated solution.</p>
|
<p>Rather than just pick a game I decided to make this overcomplicated solution.</p>
|
||||||
<p>I hope you enjoy using it!</p>
|
<p>I hope you enjoy using it!</p>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<h1>There was an error searching for gamesTable! 🤦</h1>
|
<h1>There was an error searching for games! 🤦</h1>
|
||||||
<h2>Please try again later. 🙇</h2>
|
<h2>Please try again later. 🙇</h2>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ 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 gamesTable', 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')
|
||||||
|
|
@ -29,13 +29,13 @@ async function searchForGames(locals: App.Locals, eventFetch: typeof fetch, urlQ
|
||||||
}
|
}
|
||||||
|
|
||||||
const games = await response.json()
|
const games = await response.json()
|
||||||
console.log('gamesTable 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 gamesTable 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export let data
|
||||||
const { games, totalCount } = data.searchData
|
const { games, totalCount } = data.searchData
|
||||||
|
|
||||||
console.log('data found', data)
|
console.log('data found', data)
|
||||||
console.log('found gamesTable', games)
|
console.log('found games', games)
|
||||||
console.log('found totalCount', totalCount)
|
console.log('found totalCount', totalCount)
|
||||||
|
|
||||||
const form = superForm(data.form, {
|
const form = superForm(data.form, {
|
||||||
|
|
@ -65,7 +65,7 @@ function handleListStyle(event) {
|
||||||
<Game {game} />
|
<Game {game} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<h2>Sorry no gamesTable found!</h2>
|
<h2>Sorry no games found!</h2>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Pagination.Root count={totalCount} perPage={pageSize} let:pages let:currentPage>
|
<Pagination.Root count={totalCount} perPage={pageSize} let:pages let:currentPage>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ let { data, children } = $props()
|
||||||
<div class="quote-wrapper">
|
<div class="quote-wrapper">
|
||||||
<blockquote class="quote">
|
<blockquote class="quote">
|
||||||
<p>
|
<p>
|
||||||
"How many gamesTable do I own? What was the last one I played? What haven't I played in a long
|
"How many games do I own? What was the last one I played? What haven't I played in a long
|
||||||
time? If this sounds like you then Bored Game is your new best friend."
|
time? If this sounds like you then Bored Game is your new best friend."
|
||||||
</p>
|
</p>
|
||||||
<footer>Bradley</footer>
|
<footer>Bradley</footer>
|
||||||
|
|
|
||||||
|
|
@ -12,21 +12,11 @@ export const load: PageServerLoad = async (event) => {
|
||||||
const authedUser = await locals.getAuthedUser()
|
const authedUser = await locals.getAuthedUser()
|
||||||
|
|
||||||
if (authedUser) {
|
if (authedUser) {
|
||||||
|
console.log('user already signed in')
|
||||||
const message = { type: 'success', message: 'You are already signed in' } as const
|
const message = { type: 'success', message: 'You are already signed in' } as const
|
||||||
throw redirect('/', message, event)
|
throw redirect('/', message, event)
|
||||||
|
// redirect(302, '/', message, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (userFullyAuthenticated(user, session)) {
|
|
||||||
// const message = { type: 'success', message: 'You are already signed in' } as const;
|
|
||||||
// throw redirect('/', message, event);
|
|
||||||
// } else if (userNotFullyAuthenticated(user, session)) {
|
|
||||||
// await lucia.invalidateSession(locals.session!.id!);
|
|
||||||
// const sessionCookie = lucia.createBlankSessionCookie();
|
|
||||||
// cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
// path: '.',
|
|
||||||
// ...sessionCookie.attributes,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
const form = await superValidate(event, zod(signinUsernameDto))
|
const form = await superValidate(event, zod(signinUsernameDto))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -59,81 +49,6 @@ export const actions: Actions = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// let session;
|
|
||||||
// let sessionCookie;
|
|
||||||
// const user: Users | undefined = await db.query.usersTable.findFirst({
|
|
||||||
// where: or(eq(usersTable.username, form.data.username), eq(usersTable.email, form.data.username)),
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (!user) {
|
|
||||||
// form.data.password = '';
|
|
||||||
// return setError(form, 'username', 'Your username or password is incorrect.');
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let twoFactorDetails;
|
|
||||||
//
|
|
||||||
try {
|
|
||||||
// const password = form.data.password;
|
|
||||||
// console.log('user', JSON.stringify(user, null, 2));
|
|
||||||
//
|
|
||||||
// if (!user?.hashed_password) {
|
|
||||||
// console.log('invalid username/password');
|
|
||||||
// form.data.password = '';
|
|
||||||
// return setError(form, 'password', 'Your username or password is incorrect.');
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const validPassword = await new Argon2id().verify(user.hashed_password, password);
|
|
||||||
// if (!validPassword) {
|
|
||||||
// console.log('invalid password');
|
|
||||||
// form.data.password = '';
|
|
||||||
// return setError(form, 'password', 'Your username or password is incorrect.');
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// console.log('ip', locals.ip);
|
|
||||||
// console.log('country', locals.country);
|
|
||||||
//
|
|
||||||
// twoFactorDetails = await db.query.twoFactor.findFirst({
|
|
||||||
// where: eq(twoFactor.userId, user?.id),
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (twoFactorDetails?.secret && twoFactorDetails?.enabled) {
|
|
||||||
// await db.update(twoFactor).set({
|
|
||||||
// initiatedTime: new Date(),
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// session = await lucia.createSession(user.id, {
|
|
||||||
// ip_country: locals.country,
|
|
||||||
// ip_address: locals.ip,
|
|
||||||
// twoFactorAuthEnabled:
|
|
||||||
// twoFactorDetails?.enabled &&
|
|
||||||
// twoFactorDetails?.secret !== null &&
|
|
||||||
// twoFactorDetails?.secret !== '',
|
|
||||||
// isTwoFactorAuthenticated: false,
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// session = await lucia.createSession(user.id, {
|
|
||||||
// ip_country: locals.country,
|
|
||||||
// ip_address: locals.ip,
|
|
||||||
// twoFactorAuthEnabled: false,
|
|
||||||
// isTwoFactorAuthenticated: false,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// console.log('logging in session', session);
|
|
||||||
// sessionCookie = lucia.createSessionCookie(session.id);
|
|
||||||
// console.log('logging in session cookie', sessionCookie);
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: need to return error message to the client
|
|
||||||
console.error(e)
|
|
||||||
form.data.password = ''
|
|
||||||
return setError(form, '', 'Your username or password is incorrect.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('setting session cookie', sessionCookie);
|
|
||||||
// event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
// path: '.',
|
|
||||||
// ...sessionCookie.attributes,
|
|
||||||
// });
|
|
||||||
|
|
||||||
form.data.username = ''
|
form.data.username = ''
|
||||||
form.data.password = ''
|
form.data.password = ''
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$lib/styles/app.pcss'
|
import '$lib/styles/app.pcss'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import toast, { Toaster } from 'svelte-french-toast'
|
|
||||||
import { MetaTags } from 'svelte-meta-tags'
|
import { MetaTags } from 'svelte-meta-tags'
|
||||||
import { getFlash } from 'sveltekit-flash-message/client'
|
import { getFlash } from 'sveltekit-flash-message/client'
|
||||||
import 'iconify-icon'
|
import 'iconify-icon'
|
||||||
import { onNavigate } from '$app/navigation'
|
import { onNavigate } from '$app/navigation'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import Analytics from '$components/Analytics.svelte'
|
import Analytics from '$components/Analytics.svelte'
|
||||||
|
import PlausibleAnalytics from '$components/PlausibleAnalytics.svelte'
|
||||||
|
import { Toaster } from '$lib/components/ui/sonner'
|
||||||
import PageLoadingIndicator from '$lib/page_loading_indicator.svelte'
|
import PageLoadingIndicator from '$lib/page_loading_indicator.svelte'
|
||||||
|
import { toastMessage } from '$lib/utils/superforms.js'
|
||||||
import { theme } from '$state/theme'
|
import { theme } from '$state/theme'
|
||||||
|
// import { ModeWatcher } from 'mode-watcher'
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production'
|
const dev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
|
@ -18,12 +21,12 @@ const { user } = data
|
||||||
|
|
||||||
const metaTags = $derived({
|
const metaTags = $derived({
|
||||||
titleTemplate: '%s | Bored Game',
|
titleTemplate: '%s | Bored Game',
|
||||||
description: 'Bored Game, keep track of your gamesTable.',
|
description: 'Bored Game, keep track of your games.',
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: 'website',
|
type: 'website',
|
||||||
titleTemplate: '%s | Bored Game',
|
titleTemplate: '%s | Bored Game',
|
||||||
locale: 'en_US',
|
locale: 'en_US',
|
||||||
description: 'Bored Game, keep track of your gamesTable',
|
description: 'Bored Game, keep track of your games',
|
||||||
},
|
},
|
||||||
...$page.data.metaTagsChild,
|
...$page.data.metaTagsChild,
|
||||||
})
|
})
|
||||||
|
|
@ -41,15 +44,9 @@ onMount(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
console.log('flash', $flash)
|
||||||
if ($flash) {
|
if ($flash) {
|
||||||
if ($flash.type === 'success') {
|
toastMessage({ type: $flash.type, text: $flash.message })
|
||||||
toast.success($flash.message)
|
|
||||||
} else {
|
|
||||||
toast.error($flash.message, {
|
|
||||||
duration: 5000,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clearing the flash message could sometimes
|
// Clearing the flash message could sometimes
|
||||||
// be required here to avoid double-toasting.
|
// be required here to avoid double-toasting.
|
||||||
flash.set(undefined)
|
flash.set(undefined)
|
||||||
|
|
@ -70,9 +67,11 @@ onNavigate(async (navigation) => {
|
||||||
|
|
||||||
{#if !dev}
|
{#if !dev}
|
||||||
<Analytics />
|
<Analytics />
|
||||||
|
<PlausibleAnalytics />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<MetaTags {...metaTags} />
|
<MetaTags {...metaTags} />
|
||||||
<PageLoadingIndicator />
|
<PageLoadingIndicator />
|
||||||
|
<!-- <ModeWatcher /> -->
|
||||||
|
<Toaster />
|
||||||
{@render children()}
|
{@render children()}
|
||||||
<Toaster />
|
|
||||||
Loading…
Reference in a new issue