mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Adding paraglide for i18n support and dropdown to header. Formatting code based on a new SvelteKit Svelte5 app.
This commit is contained in:
parent
b5dae43ba4
commit
3204b0b28b
72 changed files with 2904 additions and 952 deletions
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
11
.prettierrc
11
.prettierrc
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
boredgame.localhost {
|
||||
reverse_proxy / localhost:4173
|
||||
}
|
||||
116
biome.json
116
biome.json
|
|
@ -1,55 +1,65 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 150,
|
||||
"attributePosition": "auto",
|
||||
"ignore": [
|
||||
"**/.DS_Store",
|
||||
"**/node_modules",
|
||||
"./build",
|
||||
"./.svelte-kit",
|
||||
"./package",
|
||||
"**/.env",
|
||||
"**/.env.*",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock"
|
||||
]
|
||||
},
|
||||
"organizeImports": { "enabled": true },
|
||||
"linter": { "enabled": true, "rules": { "recommended": true } },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.svelte"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": { "ignoreUnknown": false, "ignore": [] },
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 150,
|
||||
"attributePosition": "auto",
|
||||
"ignore": [
|
||||
"**/.DS_Store",
|
||||
"**/node_modules",
|
||||
"./build",
|
||||
"./.svelte-kit",
|
||||
"./package",
|
||||
"**/.env",
|
||||
"**/.env.*",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock",
|
||||
"**/paraglide/**"
|
||||
]
|
||||
},
|
||||
"organizeImports": { "enabled": true },
|
||||
"linter": { "enabled": true, "rules": { "recommended": true } },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "all",
|
||||
"indentStyle": "space",
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 150,
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.svelte"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
4
messages/en.json
Normal file
4
messages/en.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from en!"
|
||||
}
|
||||
4
messages/es.json
Normal file
4
messages/es.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from es!"
|
||||
}
|
||||
22
package.json
22
package.json
|
|
@ -12,7 +12,7 @@
|
|||
"build": "vite build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "vite preview",
|
||||
"test": "playwright test",
|
||||
"test:e2e": "playwright test",
|
||||
"test:ui": "svelte-kit sync && playwright test --ui",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
|
|
@ -23,14 +23,14 @@
|
|||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/enhanced-img": "^0.3.10",
|
||||
"@sveltejs/kit": "^2.8.0",
|
||||
"@sveltejs/kit": "^2.8.1",
|
||||
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^20.17.6",
|
||||
|
|
@ -42,9 +42,6 @@
|
|||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "^0.21.16",
|
||||
"drizzle-kit": "^0.27.2",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "2.36.0-next.13",
|
||||
"formsnap": "^1.0.1",
|
||||
"just-clone": "^6.2.0",
|
||||
"just-debounce-it": "^3.2.0",
|
||||
|
|
@ -52,12 +49,12 @@
|
|||
"lucide-svelte": "^0.408.0",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-load-config": "^5.1.0",
|
||||
"postcss-preset-env": "^9.6.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"prettier-plugin-svelte": "^3.2.8",
|
||||
"svelte": "5.0.0-next.175",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-headless-table": "^0.18.3",
|
||||
|
|
@ -72,7 +69,7 @@
|
|||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^5.4.11",
|
||||
"vitest": "^1.6.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
|
@ -84,6 +81,7 @@
|
|||
"@hono/zod-validator": "^0.2.2",
|
||||
"@iconify-icons/line-md": "^1.2.30",
|
||||
"@iconify-icons/mdi": "^1.2.48",
|
||||
"@inlang/paraglide-sveltekit": "^0.11.1",
|
||||
"@internationalized/date": "^3.5.6",
|
||||
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
||||
"@lukeed/uuid": "^2.0.1",
|
||||
|
|
@ -102,17 +100,17 @@
|
|||
"@sveltejs/adapter-vercel": "^5.4.7",
|
||||
"@types/feather-icons": "^4.29.4",
|
||||
"boardgamegeekclient": "^1.9.1",
|
||||
"bullmq": "^5.25.4",
|
||||
"bullmq": "^5.25.6",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cookie": "^1.0.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-expand": "^11.0.6",
|
||||
"dotenv-expand": "^11.0.7",
|
||||
"drizzle-orm": "^0.36.1",
|
||||
"drizzle-zod": "^0.5.1",
|
||||
"feather-icons": "^4.29.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"hono": "^4.6.9",
|
||||
"hono": "^4.6.10",
|
||||
"hono-pino": "^0.3.0",
|
||||
"hono-rate-limiter": "^0.4.0",
|
||||
"hono-zod-openapi": "^0.4.2",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default defineConfig({
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'e2e',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
|
||||
});
|
||||
|
|
|
|||
1790
pnpm-lock.yaml
1790
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,5 @@
|
|||
const tailwindcss = require("tailwindcss");
|
||||
const tailwindNesting = require('tailwindcss/nesting');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const postcssPresetEnv = require('postcss-preset-env');
|
||||
const atImport = require('postcss-import');
|
||||
|
||||
|
|
|
|||
1
project.inlang/.gitignore
vendored
Normal file
1
project.inlang/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
cache
|
||||
1
project.inlang/project_id
Normal file
1
project.inlang/project_id
Normal file
|
|
@ -0,0 +1 @@
|
|||
927922ef9fe834ca9a0dea407aa01519bbcb60dadb84c6bd9cafd324f6108c44
|
||||
20
project.inlang/settings.json
Normal file
20
project.inlang/settings.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{languageTag}.json"
|
||||
},
|
||||
"sourceLanguageTag": "en",
|
||||
"languageTags": [
|
||||
"en",
|
||||
"es"
|
||||
]
|
||||
}
|
||||
67
src/app.d.ts
vendored
67
src/app.d.ts
vendored
|
|
@ -1,44 +1,41 @@
|
|||
import type { ApiClient } from '$lib/server/api';
|
||||
import type { Users } from '$lib/server/api/databases/postgres/tables';
|
||||
import type { parseApiResponse } from '$lib/utils/api';
|
||||
import type { User } from 'lucia';
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
|
||||
// src/app.d.ts
|
||||
declare global {
|
||||
namespace App {
|
||||
interface PageData {
|
||||
flash?: {
|
||||
type: 'success' | 'error' | 'info';
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
interface Locals {
|
||||
api: ApiClient['api'];
|
||||
parseApiResponse: typeof parseApiResponse;
|
||||
getAuthedUser: () => Promise<Returned<User> | null>;
|
||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||
}
|
||||
namespace Superforms {
|
||||
type Message = {
|
||||
type: 'error' | 'success' | 'info';
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
interface Error {
|
||||
code?: string;
|
||||
errorId?: string;
|
||||
}
|
||||
}
|
||||
namespace App {
|
||||
interface PageData {
|
||||
flash?: {
|
||||
type: 'success' | 'error' | 'info';
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
interface Locals {
|
||||
api: ApiClient['api'];
|
||||
parseApiResponse: typeof parseApiResponse;
|
||||
getAuthedUser: () => Promise<Returned<Users> | null>;
|
||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||
}
|
||||
namespace Superforms {
|
||||
type Message = {
|
||||
type: 'error' | 'success' | 'info';
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
interface Error {
|
||||
code?: string;
|
||||
errorId?: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Document {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
startViewTransition: (callback: never) => void; // Add your custom property/method here
|
||||
}
|
||||
interface Document {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
startViewTransition: (callback: never) => void; // Add your custom property/method here
|
||||
}
|
||||
}
|
||||
|
||||
// THIS IS IMPORTANT!!!
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
|
||||
<head>
|
||||
<meta name="robots" content="noindex, nofollow"/>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="description" content="Bored? Find a game! Bored Game!"/>
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon-bored-game.svg"/>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="svelte">%sveltekit.body%</div>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
import 'reflect-metadata';
|
||||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import type { ApiRoutes } from '$lib/server/api';
|
||||
import { parseApiResponse } from '$lib/utils/api';
|
||||
import { type Handle, redirect } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { hc } from 'hono/client';
|
||||
import "reflect-metadata";
|
||||
import { StatusCodes } from "$lib/constants/status-codes";
|
||||
import type { ApiRoutes } from "$lib/server/api";
|
||||
import { parseApiResponse } from "$lib/utils/api";
|
||||
import { type Handle, redirect } from "@sveltejs/kit";
|
||||
import { sequence } from "@sveltejs/kit/hooks";
|
||||
import { hc } from "hono/client";
|
||||
import { i18n } from "$lib/i18n";
|
||||
|
||||
const handleParaglide: Handle = i18n.handle();
|
||||
|
||||
const apiClient: Handle = async ({ event, resolve }) => {
|
||||
/* ------------------------------ Register api ------------------------------ */
|
||||
const { api } = hc<ApiRoutes>('/', {
|
||||
const { api } = hc<ApiRoutes>("/", {
|
||||
fetch: event.fetch,
|
||||
headers: {
|
||||
'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(),
|
||||
host: event.request.headers.get('host') || '',
|
||||
"x-forwarded-for": event.url.host.includes("sveltekit-prerender") ? "127.0.0.1" : event.getClientAddress(),
|
||||
host: event.request.headers.get("host") || "",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -25,7 +28,7 @@ const apiClient: Handle = async ({ event, resolve }) => {
|
|||
async function getAuthedUserOrThrow() {
|
||||
const { data } = await api.user.$get().then(parseApiResponse);
|
||||
if (!data || !data.user) {
|
||||
throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
|
||||
throw redirect(StatusCodes.TEMPORARY_REDIRECT, "/");
|
||||
}
|
||||
return data?.user;
|
||||
}
|
||||
|
|
@ -40,4 +43,4 @@ const apiClient: Handle = async ({ event, resolve }) => {
|
|||
return await resolve(event);
|
||||
};
|
||||
|
||||
export const handle: Handle = sequence(apiClient);
|
||||
export const handle: Handle = sequence(apiClient, handleParaglide);
|
||||
|
|
|
|||
2
src/hooks.ts
Normal file
2
src/hooks.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { i18n } from '$lib/i18n';
|
||||
export const reroute = i18n.reroute();
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { toastMessage } from '$lib/utils/superforms'; // Adjust the path if necessary
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { toastMessage } from '$lib/utils/superforms'; // Adjust the path if necessary
|
||||
|
||||
const { codeContent, language }: { codeContent: string; language: string } = $props()
|
||||
const { codeContent, language }: { codeContent: string; language: string } = $props();
|
||||
|
||||
// Function to copy code to clipboard
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard
|
||||
.writeText(codeContent)
|
||||
.then(() => {
|
||||
toastMessage({ text: 'Copied to clipboard!', type: 'success' })
|
||||
toastMessage({ text: 'Copied to clipboard!', type: 'success' });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy: ', err)
|
||||
})
|
||||
}
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if codeContent}
|
||||
|
|
@ -24,7 +24,7 @@ const copyToClipboard = () => {
|
|||
{codeContent}
|
||||
</span>
|
||||
</code>
|
||||
<Button class="copy-button" on:click={copyToClipboard}>Copy</Button>
|
||||
<Button class="copy-button" onclick={copyToClipboard}>Copy</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,32 @@ import Logo from '$components/logo.svelte';
|
|||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte';
|
||||
import { type AvailableLanguageTag, languageTag } from '$lib/paraglide/runtime';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Users } from '$lib/server/api/databases/postgres/tables';
|
||||
|
||||
let { user = null } = $props();
|
||||
let { user = null }: { user: Users | null } = $props();
|
||||
|
||||
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)');
|
||||
|
||||
let language = $derived.by(() => {
|
||||
switch (languageTag()) {
|
||||
case 'en':
|
||||
return '🇺🇸';
|
||||
case 'es':
|
||||
return '🇲🇽';
|
||||
default:
|
||||
return '🇺🇸';
|
||||
}
|
||||
});
|
||||
|
||||
function switchToLanguage(newLanguage: AvailableLanguageTag) {
|
||||
const canonicalPath = i18n.route($page.url.pathname);
|
||||
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
|
||||
goto(localisedPath);
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
|
|
@ -25,9 +47,23 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
|
|||
<a href="/login"> <span class="flex-auto">Login</span></a>
|
||||
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
|
||||
{/if}
|
||||
{@render languageDropdown()}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{#snippet languageDropdown()}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<span class="flex-auto">{language}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<button onclick={() => switchToLanguage('en')}><DropdownMenu.Item><span>🇺🇸 English</span></DropdownMenu.Item></button>
|
||||
<DropdownMenu.Separator />
|
||||
<button onclick={() => switchToLanguage('es')}><DropdownMenu.Item><span>🇲🇽 Spanish</span></DropdownMenu.Item></button>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/snippet}
|
||||
|
||||
{#snippet userDropdown()}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<!-- Taken from carbon design system svelte -->
|
||||
<!-- https://github.com/carbon-design-system/carbon-components-svelte/blob/master/src/SkeletonPlaceholder/SkeletonPlaceholder.svelte -->
|
||||
<script lang="ts">
|
||||
export let style: string
|
||||
let style: string = $props();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
{style}
|
||||
class:bx--skeleton__placeholder={true}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
<!-- {...$$restProps}-->
|
||||
<!-- click-->
|
||||
<!-- mouseover-->
|
||||
<!-- mouseenter-->
|
||||
<!-- mouseleave-->
|
||||
/>
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.GroupHeadingProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
15
src/lib/components/ui/input-otp/index.ts
Normal file
15
src/lib/components/ui/input-otp/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import Root from "./input-otp.svelte";
|
||||
import Group from "./input-otp-group.svelte";
|
||||
import Slot from "./input-otp-slot.svelte";
|
||||
import Separator from "./input-otp-separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Slot,
|
||||
Separator,
|
||||
Root as InputOTP,
|
||||
Group as InputOTPGroup,
|
||||
Slot as InputOTPSlot,
|
||||
Separator as InputOTPSeparator,
|
||||
};
|
||||
16
src/lib/components/ui/input-otp/input-otp-group.svelte
Normal file
16
src/lib/components/ui/input-otp/input-otp-group.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn("flex items-center", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/input-otp/input-otp-separator.svelte
Normal file
19
src/lib/components/ui/input-otp/input-otp-separator.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import Dot from "lucide-svelte/icons/dot";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} role="separator" {...restProps}>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<Dot />
|
||||
{/if}
|
||||
</div>
|
||||
30
src/lib/components/ui/input-otp/input-otp-slot.svelte
Normal file
30
src/lib/components/ui/input-otp/input-otp-slot.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { PinInput as InputOTPPrimitive } from "bits-ui";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
cell,
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof InputOTPPrimitive.Cell> = $props();
|
||||
</script>
|
||||
|
||||
<InputOTPPrimitive.Cell
|
||||
{cell}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"border-input relative flex h-10 w-10 items-center justify-center border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
cell.isActive && "ring-ring ring-offset-background z-10 ring-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{cell.char}
|
||||
{#if cell.hasFakeCaret}
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</InputOTPPrimitive.Cell>
|
||||
22
src/lib/components/ui/input-otp/input-otp.svelte
Normal file
22
src/lib/components/ui/input-otp/input-otp.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { PinInput as InputOTPPrimitive } from "bits-ui";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: ComponentProps<typeof InputOTPPrimitive.Root> = $props();
|
||||
</script>
|
||||
|
||||
<InputOTPPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
class={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50 [&_input]:disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
16
src/lib/components/ui/select/select-group-heading.svelte
Normal file
16
src/lib/components/ui/select/select-group-heading.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SelectPrimitive.GroupHeadingProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDown class="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
19
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
19
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import ChevronUp from "lucide-svelte/icons/chevron-up";
|
||||
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUp class="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
6
src/lib/components/ui/sidebar/constants.ts
Normal file
6
src/lib/components/ui/sidebar/constants.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
export const SIDEBAR_WIDTH = "16rem";
|
||||
export const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
export const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
81
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
81
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { IsMobile } from "$lib/hooks/is-mobile.svelte.js";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
export type SidebarStateProps = {
|
||||
/**
|
||||
* A getter function that returns the current open state of the sidebar.
|
||||
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
|
||||
* component.
|
||||
*/
|
||||
open: Getter<boolean>;
|
||||
|
||||
/**
|
||||
* A function that sets the open state of the sidebar. To support `bind:open`, we need
|
||||
* a source of truth for changing the open state to ensure it will be synced throughout
|
||||
* the sub-components and any `bind:` references.
|
||||
*/
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
class SidebarState {
|
||||
readonly props: SidebarStateProps;
|
||||
open = $derived.by(() => this.props.open());
|
||||
openMobile = $state(false);
|
||||
setOpen: SidebarStateProps["setOpen"];
|
||||
#isMobile: IsMobile;
|
||||
state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
|
||||
|
||||
constructor(props: SidebarStateProps) {
|
||||
this.setOpen = props.setOpen;
|
||||
this.#isMobile = new IsMobile();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
// Convenience getter for checking if the sidebar is mobile
|
||||
// without this, we would need to use `sidebar.isMobile.current` everywhere
|
||||
get isMobile() {
|
||||
return this.#isMobile.current;
|
||||
}
|
||||
|
||||
// Event handler to apply to the `<svelte:window>`
|
||||
handleShortcutKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
setOpenMobile = (value: boolean) => {
|
||||
this.openMobile = value;
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
return this.#isMobile.current
|
||||
? (this.openMobile = !this.openMobile)
|
||||
: this.setOpen(!this.open);
|
||||
};
|
||||
}
|
||||
|
||||
const SYMBOL_KEY = "scn-sidebar";
|
||||
|
||||
/**
|
||||
* Instantiates a new `SidebarState` instance and sets it in the context.
|
||||
*
|
||||
* @param props The constructor props for the `SidebarState` class.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function setSidebar(props: SidebarStateProps): SidebarState {
|
||||
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the `SidebarState` instance from the context. This is a class instance,
|
||||
* so you cannot destructure it.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function useSidebar(): SidebarState {
|
||||
return getContext(Symbol.for(SYMBOL_KEY));
|
||||
}
|
||||
75
src/lib/components/ui/sidebar/index.ts
Normal file
75
src/lib/components/ui/sidebar/index.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { useSidebar } from "./context.svelte.js";
|
||||
import Content from "./sidebar-content.svelte";
|
||||
import Footer from "./sidebar-footer.svelte";
|
||||
import GroupAction from "./sidebar-group-action.svelte";
|
||||
import GroupContent from "./sidebar-group-content.svelte";
|
||||
import GroupLabel from "./sidebar-group-label.svelte";
|
||||
import Group from "./sidebar-group.svelte";
|
||||
import Header from "./sidebar-header.svelte";
|
||||
import Input from "./sidebar-input.svelte";
|
||||
import Inset from "./sidebar-inset.svelte";
|
||||
import MenuAction from "./sidebar-menu-action.svelte";
|
||||
import MenuBadge from "./sidebar-menu-badge.svelte";
|
||||
import MenuButton from "./sidebar-menu-button.svelte";
|
||||
import MenuItem from "./sidebar-menu-item.svelte";
|
||||
import MenuSkeleton from "./sidebar-menu-skeleton.svelte";
|
||||
import MenuSubButton from "./sidebar-menu-sub-button.svelte";
|
||||
import MenuSubItem from "./sidebar-menu-sub-item.svelte";
|
||||
import MenuSub from "./sidebar-menu-sub.svelte";
|
||||
import Menu from "./sidebar-menu.svelte";
|
||||
import Provider from "./sidebar-provider.svelte";
|
||||
import Rail from "./sidebar-rail.svelte";
|
||||
import Separator from "./sidebar-separator.svelte";
|
||||
import Trigger from "./sidebar-trigger.svelte";
|
||||
import Root from "./sidebar.svelte";
|
||||
|
||||
export {
|
||||
Content,
|
||||
Footer,
|
||||
Group,
|
||||
GroupAction,
|
||||
GroupContent,
|
||||
GroupLabel,
|
||||
Header,
|
||||
Input,
|
||||
Inset,
|
||||
Menu,
|
||||
MenuAction,
|
||||
MenuBadge,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuSkeleton,
|
||||
MenuSub,
|
||||
MenuSubButton,
|
||||
MenuSubItem,
|
||||
Provider,
|
||||
Rail,
|
||||
Root,
|
||||
Separator,
|
||||
//
|
||||
Root as Sidebar,
|
||||
Content as SidebarContent,
|
||||
Footer as SidebarFooter,
|
||||
Group as SidebarGroup,
|
||||
GroupAction as SidebarGroupAction,
|
||||
GroupContent as SidebarGroupContent,
|
||||
GroupLabel as SidebarGroupLabel,
|
||||
Header as SidebarHeader,
|
||||
Input as SidebarInput,
|
||||
Inset as SidebarInset,
|
||||
Menu as SidebarMenu,
|
||||
MenuAction as SidebarMenuAction,
|
||||
MenuBadge as SidebarMenuBadge,
|
||||
MenuButton as SidebarMenuButton,
|
||||
MenuItem as SidebarMenuItem,
|
||||
MenuSkeleton as SidebarMenuSkeleton,
|
||||
MenuSub as SidebarMenuSub,
|
||||
MenuSubButton as SidebarMenuSubButton,
|
||||
MenuSubItem as SidebarMenuSubItem,
|
||||
Provider as SidebarProvider,
|
||||
Rail as SidebarRail,
|
||||
Separator as SidebarSeparator,
|
||||
Trigger as SidebarTrigger,
|
||||
Trigger,
|
||||
useSidebar,
|
||||
};
|
||||
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="content"
|
||||
class={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="footer"
|
||||
class={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
36
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLButtonAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const propObj = $derived({
|
||||
class: cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
),
|
||||
"data-sidebar": "group-action",
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: propObj })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...propObj}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="group-content"
|
||||
class={cn("w-full text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
34
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
34
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
),
|
||||
"data-sidebar": "group-label",
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
21
src/lib/components/ui/sidebar/sidebar-group.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-group.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="group"
|
||||
class={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
21
src/lib/components/ui/sidebar/sidebar-header.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-header.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="header"
|
||||
class={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/sidebar/sidebar-input.svelte
Normal file
23
src/lib/components/ui/sidebar/sidebar-input.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Input> = $props();
|
||||
</script>
|
||||
|
||||
<Input
|
||||
bind:ref
|
||||
bind:value
|
||||
data-sidebar="input"
|
||||
class={cn(
|
||||
"bg-background focus-visible:ring-sidebar-ring h-8 w-full shadow-none focus-visible:ring-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
24
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
24
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<main
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"bg-background relative flex min-h-svh flex-1 flex-col",
|
||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</main>
|
||||
43
src/lib/components/ui/sidebar/sidebar-menu-action.svelte
Normal file
43
src/lib/components/ui/sidebar/sidebar-menu-action.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
showOnHover = false,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLButtonAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
showOnHover?: boolean;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
),
|
||||
"data-sidebar": "menu-action",
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
29
src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
Normal file
29
src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-badge"
|
||||
class={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
97
src/lib/components/ui/sidebar/sidebar-menu-button.svelte
Normal file
97
src/lib/components/ui/sidebar/sidebar-menu-button.svelte
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
export const sidebarMenuButtonVariants = tv({
|
||||
base: "peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type SidebarMenuButtonVariant = VariantProps<
|
||||
typeof sidebarMenuButtonVariants
|
||||
>["variant"];
|
||||
export type SidebarMenuButtonSize = VariantProps<typeof sidebarMenuButtonVariants>["size"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import { mergeProps, type WithElementRef, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import type { ComponentProps, Snippet } from "svelte";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { useSidebar } from "./context.svelte.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
child,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
isActive = false,
|
||||
tooltipContent,
|
||||
tooltipContentProps,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
|
||||
isActive?: boolean;
|
||||
variant?: SidebarMenuButtonVariant;
|
||||
size?: SidebarMenuButtonSize;
|
||||
tooltipContent?: Snippet;
|
||||
tooltipContentProps?: WithoutChildrenOrChild<ComponentProps<typeof Tooltip.Content>>;
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
const buttonProps = $derived({
|
||||
class: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
||||
"data-sidebar": "menu-button",
|
||||
"data-size": size,
|
||||
"data-active": isActive,
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet Button({ props }: { props?: Record<string, unknown> })}
|
||||
{@const mergedProps = mergeProps(buttonProps, props)}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if !tooltipContent}
|
||||
{@render Button({})}
|
||||
{:else}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
{@render Button({ props })}
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={sidebar.state !== "collapsed" || sidebar.isMobile}
|
||||
children={tooltipContent}
|
||||
{...tooltipContentProps}
|
||||
/>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
21
src/lib/components/ui/sidebar/sidebar-menu-item.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-menu-item.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLLIElement>, HTMLLIElement> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-item"
|
||||
class={cn("group/menu-item relative", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
36
src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
showIcon = false,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
showIcon?: boolean;
|
||||
} = $props();
|
||||
|
||||
// Random width between 50% and 90%
|
||||
const width = `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
class={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if showIcon}
|
||||
<Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
||||
{/if}
|
||||
<Skeleton
|
||||
class="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style="--skeleton-width: {width};"
|
||||
/>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
43
src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
43
src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
class: className,
|
||||
size = "md",
|
||||
isActive,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
),
|
||||
"data-sidebar": "menu-sub-button",
|
||||
"data-size": size,
|
||||
"data-active": isActive,
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<a bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
14
src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
14
src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLLIElement>> = $props();
|
||||
</script>
|
||||
|
||||
<li bind:this={ref} data-sidebar="menu-sub-item" {...restProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
25
src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
Normal file
25
src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-sub"
|
||||
class={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
21
src/lib/components/ui/sidebar/sidebar-menu.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-menu.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>, HTMLUListElement> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-sidebar="menu"
|
||||
class={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
59
src/lib/components/ui/sidebar/sidebar-provider.svelte
Normal file
59
src/lib/components/ui/sidebar/sidebar-provider.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<script lang="ts">
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import {
|
||||
SIDEBAR_COOKIE_MAX_AGE,
|
||||
SIDEBAR_COOKIE_NAME,
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON,
|
||||
} from "./constants.js";
|
||||
import { setSidebar } from "./context.svelte.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
open = $bindable(true),
|
||||
onOpenChange = () => {},
|
||||
controlledOpen = false,
|
||||
class: className,
|
||||
style,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
controlledOpen?: boolean;
|
||||
} = $props();
|
||||
|
||||
const sidebar = setSidebar({
|
||||
open: () => open,
|
||||
setOpen: (value: boolean) => {
|
||||
if (controlledOpen) {
|
||||
onOpenChange(value);
|
||||
} else {
|
||||
open = value;
|
||||
onOpenChange(value);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
|
||||
|
||||
<Tooltip.Provider delayDuration={0}>
|
||||
<div
|
||||
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
|
||||
class={cn(
|
||||
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
)}
|
||||
bind:this={ref}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
36
src/lib/components/ui/sidebar/sidebar-rail.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-rail.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { useSidebar } from "./context.svelte.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onclick={() => sidebar.toggle()}
|
||||
title="Toggle Sidebar"
|
||||
class={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
18
src/lib/components/ui/sidebar/sidebar-separator.svelte
Normal file
18
src/lib/components/ui/sidebar/sidebar-separator.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Separator> = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-sidebar="separator"
|
||||
class={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
34
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
34
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import PanelLeft from "lucide-svelte/icons/panel-left";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { useSidebar } from "./context.svelte.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
onclick,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Button> & {
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
onclick?.(e);
|
||||
sidebar.toggle();
|
||||
}}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn("h-7 w-7", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span class="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
98
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
98
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts">
|
||||
import * as Sheet from "$lib/components/ui/sheet/index.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { SIDEBAR_WIDTH_MOBILE } from "./constants.js";
|
||||
import { useSidebar } from "./context.svelte.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
{#if collapsible === "none"}
|
||||
<div
|
||||
class={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
|
||||
className
|
||||
)}
|
||||
bind:this={ref}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{:else if sidebar.isMobile}
|
||||
<Sheet.Root
|
||||
controlledOpen
|
||||
open={sidebar.openMobile}
|
||||
onOpenChange={sidebar.setOpenMobile}
|
||||
{...restProps}
|
||||
>
|
||||
<Sheet.Content
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
class="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
|
||||
style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
|
||||
{side}
|
||||
>
|
||||
<div class="flex h-full w-full flex-col">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
{:else}
|
||||
<div
|
||||
bind:this={ref}
|
||||
class="text-sidebar-foreground group peer hidden md:block"
|
||||
data-state={sidebar.state}
|
||||
data-collapsible={sidebar.state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
<!-- This is what handles the sidebar gap on desktop -->
|
||||
<div
|
||||
class={cn(
|
||||
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
class={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
7
src/lib/components/ui/skeleton/index.ts
Normal file
7
src/lib/components/ui/skeleton/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Root from "./skeleton.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Skeleton,
|
||||
};
|
||||
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { WithElementRef, WithoutChildren } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("bg-muted animate-pulse rounded-md", className)}
|
||||
{...restProps}
|
||||
></div>
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
import {z} from "zod";
|
||||
import {refinePasswords} from "$lib/validations/account";
|
||||
import { refinePasswords } from '$lib/validations/account';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const signupUsernameEmailDto = z.object({
|
||||
firstName: z.string().trim().optional(),
|
||||
lastName: z.string().trim().optional(),
|
||||
email: z.string()
|
||||
.trim()
|
||||
.max(64, {message: 'Email must be less than 64 characters'})
|
||||
.email({message: 'Please enter a valid email'})
|
||||
.optional(),
|
||||
username: z
|
||||
export const signupUsernameEmailDto = z
|
||||
.object({
|
||||
firstName: z.string().trim().optional(),
|
||||
lastName: z.string().trim().optional(),
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'}),
|
||||
password: z.string({required_error: 'Password is required'}).trim(),
|
||||
confirm_password: z.string({required_error: 'Confirm Password is required'}).trim()
|
||||
.max(64, { message: 'Email must be less than 64 characters' })
|
||||
.refine((value) => !value || z.string().email().safeParse(value).success, {
|
||||
message: 'Please enter a valid email',
|
||||
})
|
||||
.optional(),
|
||||
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
|
||||
})
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
return refinePasswords(confirm_password, password, ctx);
|
||||
});
|
||||
|
||||
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>
|
||||
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>;
|
||||
|
|
|
|||
27
src/lib/hooks/is-mobile.svelte.ts
Normal file
27
src/lib/hooks/is-mobile.svelte.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { untrack } from "svelte";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export class IsMobile {
|
||||
#current = $state<boolean>(false);
|
||||
|
||||
constructor() {
|
||||
$effect(() => {
|
||||
return untrack(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
this.#current = window.innerWidth < MOBILE_BREAKPOINT;
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
onChange();
|
||||
return () => {
|
||||
mql.removeEventListener("change", onChange);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.#current;
|
||||
}
|
||||
}
|
||||
3
src/lib/i18n.ts
Normal file
3
src/lib/i18n.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import * as runtime from '$lib/paraglide/runtime';
|
||||
import { createI18n } from '@inlang/paraglide-sveltekit';
|
||||
export const i18n = createI18n(runtime);
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
import {z} from "zod";
|
||||
import {refinePasswords} from "$lib/validations/account";
|
||||
import { refinePasswords } from '$lib/validations/account';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const signupUsernameEmailDto = z.object({
|
||||
firstName: z.string().trim().optional(),
|
||||
lastName: z.string().trim().optional(),
|
||||
email: z.string()
|
||||
.trim()
|
||||
.max(64, {message: 'Email must be less than 64 characters'})
|
||||
.email({message: 'Please enter a valid email'})
|
||||
.optional(),
|
||||
username: z
|
||||
export const signupUsernameEmailDto = z
|
||||
.object({
|
||||
firstName: z.string().trim().optional(),
|
||||
lastName: z.string().trim().optional(),
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'}),
|
||||
password: z.string({required_error: 'Password is required'}).trim(),
|
||||
confirm_password: z.string({required_error: 'Confirm Password is required'}).trim()
|
||||
.max(64, { message: 'Email must be less than 64 characters' })
|
||||
.refine((value) => !value || z.string().email().safeParse(value).success, {
|
||||
message: 'Please enter a valid email',
|
||||
})
|
||||
.optional(),
|
||||
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
|
||||
})
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
return refinePasswords(confirm_password, password, ctx);
|
||||
});
|
||||
|
||||
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>
|
||||
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { Button } from '$components/ui/button';
|
||||
import * as Card from '$components/ui/card';
|
||||
import * as Form from '$components/ui/form';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { boredState } from '$lib/stores/boredState.js';
|
||||
import { receive, send } from '$lib/utils/pageCrossfade';
|
||||
import * as flashModule from 'sveltekit-flash-message/client';
|
||||
import { superForm } from 'sveltekit-superforms/client';
|
||||
import { Button } from "$components/ui/button";
|
||||
import * as Card from "$components/ui/card";
|
||||
import * as Form from "$components/ui/form";
|
||||
import { Input } from "$components/ui/input";
|
||||
import { boredState } from "$lib/stores/boredState.js";
|
||||
import { receive, send } from "$lib/utils/pageCrossfade";
|
||||
import * as flashModule from "sveltekit-flash-message/client";
|
||||
import { superForm } from "sveltekit-superforms/client";
|
||||
|
||||
let { data } = $props();
|
||||
let { data } = $props();
|
||||
|
||||
const superLoginForm = superForm(data.form, {
|
||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
onError: ({ result, flashMessage }) => {
|
||||
// Error handling for the flash message:
|
||||
// - result is the ActionResult
|
||||
// - message is the flash store (not the status message store)
|
||||
const errorMessage = result.error.message;
|
||||
flashMessage.set({ type: 'error', message: errorMessage });
|
||||
const superLoginForm = superForm(data.form, {
|
||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
onError: ({ result, flashMessage }) => {
|
||||
// Error handling for the flash message:
|
||||
// - result is the ActionResult
|
||||
// - message is the flash store (not the status message store)
|
||||
const errorMessage = result.error.message;
|
||||
flashMessage.set({ type: "error", message: errorMessage });
|
||||
},
|
||||
},
|
||||
},
|
||||
syncFlashMessage: false,
|
||||
taintedMessage: null,
|
||||
// validators: zodClient(signInSchema),
|
||||
// validationMethod: 'oninput',
|
||||
delayMs: 0,
|
||||
});
|
||||
syncFlashMessage: false,
|
||||
taintedMessage: null,
|
||||
// validators: zodClient(signInSchema),
|
||||
// validationMethod: 'oninput',
|
||||
delayMs: 0,
|
||||
});
|
||||
|
||||
const { form: loginForm, enhance } = superLoginForm;
|
||||
const { form: loginForm, enhance } = superLoginForm;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Bored Game | Login</title>
|
||||
</svelte:head>
|
||||
|
||||
<div in:receive={{ key: 'auth-card' }} out:send={{ key: 'auth-card' }}>
|
||||
<div in:receive={{ key: "auth-card" }} out:send={{ key: "auth-card" }}>
|
||||
<Card.Root class="mx-auto mt-24 max-w-sm">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-2xl">Log into your account</Card.Title>
|
||||
|
|
@ -48,13 +48,9 @@ const { form: loginForm, enhance } = superLoginForm;
|
|||
{@render oAuthButtons()}
|
||||
<p class="px-8 py-4 text-center text-sm text-muted-foreground">
|
||||
By clicking continue, you agree to our
|
||||
<a href="/terms" class="underline underline-offset-4 hover:text-primary">
|
||||
Terms of Use
|
||||
</a>
|
||||
<a href="/terms" class="underline underline-offset-4 hover:text-primary"> Terms of Use </a>
|
||||
and
|
||||
<a href="/privacy" class="underline underline-offset-4 hover:text-primary">
|
||||
Privacy Policy
|
||||
</a>.
|
||||
<a href="/privacy" class="underline underline-offset-4 hover:text-primary"> Privacy Policy </a>.
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
|
@ -85,8 +81,24 @@ const { form: loginForm, enhance } = superLoginForm;
|
|||
|
||||
{#snippet oAuthButtons()}
|
||||
<div class="grid gap-4">
|
||||
<Button href="/login/google" variant="outline" class="w-full flex items-center gap-2"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg> Google</Button>
|
||||
<Button href="/login/github" variant="outline" class="w-full flex items-center gap-2"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg> GitHub</Button>
|
||||
<Button href="/login/google" variant="outline" class="w-full flex items-center gap-2">
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
><title>Google</title>
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
<Button href="/login/github" variant="outline" class="w-full flex items-center gap-2">
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
><title>GitHub</title>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
|
|
@ -95,4 +107,4 @@ const { form: loginForm, enhance } = superLoginForm;
|
|||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const load: PageServerLoad = async (event) => {
|
|||
// }
|
||||
|
||||
return {
|
||||
form: await superValidate(zod(signupUsernameEmailDto), {
|
||||
signupForm: await superValidate(zod(signupUsernameEmailDto), {
|
||||
defaults: signUpDefaults,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,33 +6,23 @@ import * as Alert from '$lib/components/ui/alert';
|
|||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import { signupUsernameEmailDto } from '$lib/dtos/signup-username-email.dto';
|
||||
import { boredState } from '$lib/stores/boredState.js';
|
||||
import { receive, send } from '$lib/utils/pageCrossfade';
|
||||
import { ChevronsUpDown } from 'lucide-svelte';
|
||||
import { quintIn } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
import * as flashModule from 'sveltekit-flash-message/client';
|
||||
import { superForm } from 'sveltekit-superforms';
|
||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||
import { superForm } from 'sveltekit-superforms/client';
|
||||
|
||||
export let data;
|
||||
const { data } = $props();
|
||||
|
||||
const { form, errors, enhance } = superForm(data.form, {
|
||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
onError: ({ result, flashMessage }) => {
|
||||
const errorMessage = result.error.message;
|
||||
flashMessage.set({ type: 'error', message: errorMessage });
|
||||
},
|
||||
},
|
||||
taintedMessage: null,
|
||||
const signupForm = superForm(data.signupForm, {
|
||||
validators: zodClient(signupUsernameEmailDto),
|
||||
delayMs: 0,
|
||||
resetForm: false,
|
||||
});
|
||||
|
||||
let collapsibleOpen = false;
|
||||
const { form: signupFormData, errors: signupErrors, enhance: signupEnhance } = signupForm;
|
||||
|
||||
let collapsibleOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -45,26 +35,26 @@ let collapsibleOpen = false;
|
|||
<Card.Title class="text-2xl">Signup for an account</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form method="POST" action="/signup" use:enhance class="grid gap-2 mt-4">
|
||||
<form method="POST" action="/signup" use:signupEnhance class="grid gap-2 mt-4">
|
||||
<Label for="username">Username <small>(required)</small></Label>
|
||||
<Input type="text" id="username" class={$errors.username && "outline outline-destructive"} name="username"
|
||||
placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} />
|
||||
{#if $errors.username}
|
||||
<p class="text-sm text-destructive">{$errors.username}</p>
|
||||
<Input type="text" id="username" class={$signupErrors.username && "outline outline-destructive"} name="username"
|
||||
placeholder="Username" autocomplete="username" data-invalid={$signupErrors.username} bind:value={$signupFormData.username} />
|
||||
{#if $signupErrors.username}
|
||||
<p class="text-sm text-destructive">{$signupErrors.username}</p>
|
||||
{/if}
|
||||
<Label for="password">Password <small>(required)</small></Label>
|
||||
<Input type="password" id="password" class={$errors.password && "outline outline-destructive"} name="password"
|
||||
placeholder="Password" autocomplete="new-password" data-invalid={$errors.password}
|
||||
bind:value={$form.password} />
|
||||
{#if $errors.password}
|
||||
<p class="text-sm text-destructive">{$errors.password}</p>
|
||||
<Input type="password" id="password" class={$signupErrors.password && "outline outline-destructive"} name="password"
|
||||
placeholder="Password" autocomplete="new-password" data-invalid={$signupErrors.password}
|
||||
bind:value={$signupFormData.password} />
|
||||
{#if $signupErrors.password}
|
||||
<p class="text-sm text-destructive">{$signupErrors.password}</p>
|
||||
{/if}
|
||||
<Label for="confirm_password">Confirm Password <small>(required)</small></Label>
|
||||
<Input type="password" id="confirm_password" class={$errors.confirm_password && "outline outline-destructive"}
|
||||
<Input type="password" id="confirm_password" class={$signupErrors.confirm_password && "outline outline-destructive"}
|
||||
name="confirm_password" placeholder="Confirm Password" autocomplete="new-password"
|
||||
data-invalid={$errors.confirm_password} bind:value={$form.confirm_password} />
|
||||
{#if $errors.confirm_password}
|
||||
<p class="text-sm text-destructive">{$errors.confirm_password}</p>
|
||||
data-invalid={$signupErrors.confirm_password} bind:value={$signupFormData.confirm_password} />
|
||||
{#if $signupErrors.confirm_password}
|
||||
<p class="text-sm text-destructive">{$signupErrors.confirm_password}</p>
|
||||
{/if}
|
||||
<Collapsible.Root bind:open={collapsibleOpen} class="grid w-full max-w-sm items-center gap-2.5">
|
||||
<div>
|
||||
|
|
@ -79,32 +69,32 @@ let collapsibleOpen = false;
|
|||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="email">Email</Label>
|
||||
<Input type="email" id="email" class={$errors.email && "outline outline-destructive"} name="email"
|
||||
placeholder="Email" autocomplete="email" data-invalid={$errors.email} bind:value={$form.email} />
|
||||
{#if $errors.email}
|
||||
<p class="text-sm text-destructive">{$errors.email}</p>
|
||||
<Input type="email" id="email" class={$signupErrors.email && "outline outline-destructive"} name="email"
|
||||
placeholder="Email" autocomplete="email" data-invalid={$signupErrors.email} bind:value={$signupFormData.email} />
|
||||
{#if $signupErrors.email}
|
||||
<p class="text-sm text-destructive">{$signupErrors.email}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="firstName">First Name</Label>
|
||||
<Input type="text" id="firstName" class={$errors.firstName && "outline outline-destructive"} name="firstName"
|
||||
placeholder="First Name" autocomplete="given-name" data-invalid={$errors.firstName}
|
||||
bind:value={$form.firstName} />
|
||||
{#if $errors.firstName}
|
||||
<p class="text-sm text-destructive">{$errors.firstName}</p>
|
||||
<Input type="text" id="firstName" class={$signupErrors.firstName && "outline outline-destructive"} name="firstName"
|
||||
placeholder="First Name" autocomplete="given-name" data-invalid={$signupErrors.firstName}
|
||||
bind:value={$signupFormData.firstName} />
|
||||
{#if $signupErrors.firstName}
|
||||
<p class="text-sm text-destructive">{$signupErrors.firstName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
<Collapsible.Content>
|
||||
<div transition:slide|global={{ delay: 10, duration: 150, easing: quintIn }}>
|
||||
<Label for="firstName">Last Name</Label>
|
||||
<Input type="text" id="lastName" class={$errors.firstName && "outline outline-destructive"} name="lastName"
|
||||
placeholder="Last Name" autocomplete="family-name" data-invalid={$errors.lastName}
|
||||
bind:value={$form.lastName} />
|
||||
{#if $errors.lastName}
|
||||
<p class="text-sm text-destructive">{$errors.lastName}</p>
|
||||
<Input type="text" id="lastName" class={$signupErrors.firstName && "outline outline-destructive"} name="lastName"
|
||||
placeholder="Last Name" autocomplete="family-name" data-invalid={$signupErrors.lastName}
|
||||
bind:value={$signupFormData.lastName} />
|
||||
{#if $signupErrors.lastName}
|
||||
<p class="text-sm text-destructive">{$signupErrors.lastName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
|
|
@ -113,7 +103,7 @@ let collapsibleOpen = false;
|
|||
<Button type="submit">Signup</Button>
|
||||
<Button variant="link" class="text-secondary-foreground" href="/">or Cancel</Button>
|
||||
</div>
|
||||
{#if !$form.email}
|
||||
{#if !$signupFormData.email}
|
||||
<Alert.Root>
|
||||
<Alert.Title level="h3">Heads up!</Alert.Title>
|
||||
<Alert.Description>
|
||||
|
|
@ -128,41 +118,4 @@ let collapsibleOpen = false;
|
|||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.sign-up {
|
||||
display: flex;
|
||||
margin-top: 1.5rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
max-width: 24rem;
|
||||
|
||||
h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom-width: 1px;
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
scroll-margin: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,67 +1,69 @@
|
|||
<script lang="ts">
|
||||
import '$lib/styles/app.pcss';
|
||||
import { onMount } from 'svelte';
|
||||
import { MetaTags } from 'svelte-meta-tags';
|
||||
import { getFlash } from 'sveltekit-flash-message/client';
|
||||
import 'iconify-icon';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Analytics from '$components/Analytics.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import PageLoadingIndicator from '$lib/page_loading_indicator.svelte';
|
||||
import { toastMessage } from '$lib/utils/superforms.js';
|
||||
import { theme } from '$state/theme';
|
||||
// import { ModeWatcher } from 'mode-watcher'
|
||||
import '$lib/styles/app.pcss';
|
||||
import { onMount } from 'svelte';
|
||||
import { MetaTags } from 'svelte-meta-tags';
|
||||
import { getFlash } from 'sveltekit-flash-message/client';
|
||||
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
|
||||
import 'iconify-icon';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Analytics from '$components/Analytics.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import PageLoadingIndicator from '$lib/page_loading_indicator.svelte';
|
||||
import { toastMessage } from '$lib/utils/superforms.js';
|
||||
import { theme } from '$state/theme';
|
||||
import { i18n } from '$lib/i18n';
|
||||
// import { ModeWatcher } from 'mode-watcher'
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const { data, children } = $props();
|
||||
const { user } = data;
|
||||
const { data, children } = $props();
|
||||
const { user } = data;
|
||||
|
||||
const metaTags = $derived({
|
||||
titleTemplate: '%s | Bored Game',
|
||||
description: 'Bored Game, keep track of your games.',
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
const metaTags = $derived({
|
||||
titleTemplate: '%s | Bored Game',
|
||||
locale: 'en_US',
|
||||
description: 'Bored Game, keep track of your games',
|
||||
},
|
||||
...$page.data.metaTagsChild,
|
||||
});
|
||||
description: 'Bored Game, keep track of your games.',
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
titleTemplate: '%s | Bored Game',
|
||||
locale: 'en_US',
|
||||
description: 'Bored Game, keep track of your games',
|
||||
},
|
||||
...$page.data.metaTagsChild,
|
||||
});
|
||||
|
||||
const flash = getFlash(page, {
|
||||
clearOnNavigate: true,
|
||||
clearAfterMs: 3000,
|
||||
clearArray: true,
|
||||
});
|
||||
const flash = getFlash(page, {
|
||||
clearOnNavigate: true,
|
||||
clearAfterMs: 3000,
|
||||
clearArray: true,
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
// set the theme to the user's active theme
|
||||
$theme = user?.theme || 'system';
|
||||
document.querySelector('html')?.setAttribute('data-theme', $theme);
|
||||
});
|
||||
onMount(() => {
|
||||
// set the theme to the user's active theme
|
||||
$theme = user?.theme || 'system';
|
||||
document.querySelector('html')?.setAttribute('data-theme', $theme);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
console.log('flash', $flash);
|
||||
if ($flash) {
|
||||
toastMessage({ type: $flash.type, text: $flash.message });
|
||||
// Clearing the flash message could sometimes
|
||||
// be required here to avoid double-toasting.
|
||||
flash.set(undefined);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
console.log('flash', $flash);
|
||||
if ($flash) {
|
||||
toastMessage({ type: $flash.type, text: $flash.message });
|
||||
// Clearing the flash message could sometimes
|
||||
// be required here to avoid double-toasting.
|
||||
flash.set(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
onNavigate(async (navigation) => {
|
||||
if (!document.startViewTransition) return;
|
||||
onNavigate(async (navigation) => {
|
||||
if (!document.startViewTransition) return;
|
||||
|
||||
return new Promise((oldStateCaptureResolve) => {
|
||||
document.startViewTransition(async () => {
|
||||
oldStateCaptureResolve();
|
||||
await navigation.complete;
|
||||
return new Promise((oldStateCaptureResolve) => {
|
||||
document.startViewTransition(async () => {
|
||||
oldStateCaptureResolve();
|
||||
await navigation.complete;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !dev}
|
||||
|
|
@ -72,4 +74,7 @@ onNavigate(async (navigation) => {
|
|||
<PageLoadingIndicator />
|
||||
<!-- <ModeWatcher /> -->
|
||||
<Toaster />
|
||||
{@render children()}
|
||||
|
||||
<ParaglideJS {i18n}>
|
||||
{@render children()}
|
||||
</ParaglideJS>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'reflect-metadata'
|
||||
import { preprocessMeltUI } from '@melt-ui/pp'
|
||||
import adapter from '@sveltejs/adapter-node'
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
import type { Config } from "tailwindcss";
|
||||
import tailwindcssAnimate from "tailwindcss-animate";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
const config: Config = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
safelist: ["dark"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
|
|
@ -47,16 +48,46 @@ const config = {
|
|||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))"
|
||||
}
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: "hsl(var(--sidebar-background))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
primary: "hsl(var(--sidebar-primary))",
|
||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
ring: "hsl(var(--sidebar-ring))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
xl: "calc(var(--radius) + 4px)",
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)"
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...fontFamily.sans]
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--bits-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--bits-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: [tailwindcssAnimate]
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
// import { sentrySvelteKit } from "@sentry/sveltekit";
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { paraglide } from "@inlang/paraglide-sveltekit/vite";
|
||||
import { defineConfig } from 'vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
// TODO: Fix Sentry
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
// sentrySvelteKit({
|
||||
// sourceMapsUploadOptions: {
|
||||
// org: process.env.SENTRY_ORG,
|
||||
// project: process.env.SENTRY_PROJECT,
|
||||
// authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
// cleanArtifacts: true,
|
||||
// }
|
||||
// }),
|
||||
sveltekit(),
|
||||
sveltekit(), paraglide({
|
||||
project: "./project.inlang",
|
||||
outdir: "./src/lib/paraglide"
|
||||
}),
|
||||
],
|
||||
esbuild: {
|
||||
target: 'es2022',
|
||||
|
|
|
|||
Loading…
Reference in a new issue