Merge pull request #28 from BradNut/development

Development
This commit is contained in:
Bradley Shellnut 2024-10-03 21:48:46 +00:00 committed by GitHub
commit 6836ebe64c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 835 additions and 836 deletions

View file

@ -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=

View file

@ -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:

View file

@ -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",

File diff suppressed because it is too large Load diff

17
src/app.d.ts vendored
View file

@ -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;

View file

@ -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>

View file

@ -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" />

View 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>

View file

@ -1,28 +1,28 @@
<script lang="ts"> <script lang="ts">
import { type SvelteComponent, createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
// import { // import {
// Dialog, // Dialog,
// DialogDescription, // DialogDescription,
// DialogOverlay, // DialogOverlay,
// DialogTitle // DialogTitle
// } from '@rgossiaux/svelte-headlessui'; // } from '@rgossiaux/svelte-headlessui';
import { boredState } from '$lib/stores/boredState'; import { boredState } from '$lib/stores/boredState'
import { type SvelteComponent, createEventDispatcher } from 'svelte'
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;

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<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>

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<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>

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<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.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>

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.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.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 />

View file

@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View 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}
/>

View file

@ -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,
}, },

View file

@ -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(),

View file

@ -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

View file

@ -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,
},
}) })
} }

View file

@ -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),
}) })
} }

View file

@ -51,6 +51,7 @@ export class WishlistsRepository {
columns: { columns: {
cuid: true, cuid: true,
name: true, name: true,
createdAt: true,
}, },
}) })
} }

View file

@ -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) {

View 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)
}

View file

@ -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]};`
}, '') }, '')
} }

View file

@ -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'

View file

@ -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) {

View file

@ -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>
@ -10,13 +11,12 @@ let collections = data?.collections || []
<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>

View file

@ -34,7 +34,12 @@ let { children } = $props()
.security-nav { .security-nav {
display: flex; display: flex;
@media (width <= 1000px) {
display: grid;
}
nav { nav {
@media (width > 1000px) {
width: 16rem; width: 16rem;
position: sticky; position: sticky;
top: 0; top: 0;
@ -43,6 +48,7 @@ let { children } = $props()
padding: 1rem; padding: 1rem;
border-right: 1px solid #ddd; border-right: 1px solid #ddd;
height: 100vh; height: 100vh;
}
ul { ul {
list-style-type: none; list-style-type: none;

View file

@ -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

View file

@ -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')

View file

@ -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,23 +10,27 @@ 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 wishlistsTable</h2> <h2>You have no wishlists</h2>
{:else} {:else}
{#each wishlists as wishlist} {#each wishlists as wishlist}
<div class="collection grid gap-0.5"> <Card.Root class="shadow-sm hover:shadow-md transition-shadow duration-300 ease-in-out">
<h2><a href="/wishlists/{wishlist.cuid}">{wishlist.name}</a></h2> <a href="/wishlists/{wishlist.cuid}">
<h3>Created at: {new Date(wishlist.created_at).toLocaleString()}</h3> <Card.Header>
</div> <Card.Title>{wishlist.name}</Card.Title>
</Card.Header>
<Card.Content>
<h3>Created at: {new Date(wishlist.createdAt).toLocaleString()}</h3>
</Card.Content>
</a>
</Card.Root>
{/each} {/each}
{/if} {/if}
</div> </div>
</div> </div>
</div>
<style lang="postcss"> <style lang="postcss">
h1 { h1 {
@ -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));

View file

@ -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;

View file

@ -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',
}, },

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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>

View file

@ -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>

View file

@ -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 = ''

View file

@ -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 />
{@render children()} <!-- <ModeWatcher /> -->
<Toaster /> <Toaster />
{@render children()}