├── .npmrc
├── static
├── robots.txt
├── favicon.png
└── fonts
│ ├── Sora-VariableFont.ttf
│ └── Montserrat-VariableFont.ttf
├── src
├── lib
│ ├── components
│ │ ├── ui
│ │ │ ├── sonner
│ │ │ │ ├── index.ts
│ │ │ │ └── sonner.svelte
│ │ │ ├── input
│ │ │ │ ├── index.ts
│ │ │ │ └── input.svelte
│ │ │ ├── label
│ │ │ │ ├── index.ts
│ │ │ │ └── label.svelte
│ │ │ ├── slider
│ │ │ │ ├── index.ts
│ │ │ │ └── slider.svelte
│ │ │ ├── switch
│ │ │ │ ├── index.ts
│ │ │ │ └── switch.svelte
│ │ │ ├── checkbox
│ │ │ │ ├── index.ts
│ │ │ │ └── checkbox.svelte
│ │ │ ├── progress
│ │ │ │ ├── index.ts
│ │ │ │ └── progress.svelte
│ │ │ ├── separator
│ │ │ │ ├── index.ts
│ │ │ │ └── separator.svelte
│ │ │ ├── skeleton
│ │ │ │ ├── index.ts
│ │ │ │ └── skeleton.svelte
│ │ │ ├── textarea
│ │ │ │ ├── index.ts
│ │ │ │ └── textarea.svelte
│ │ │ ├── badge
│ │ │ │ ├── index.ts
│ │ │ │ └── badge.svelte
│ │ │ ├── kbd
│ │ │ │ ├── index.ts
│ │ │ │ ├── kbd-group.svelte
│ │ │ │ └── kbd.svelte
│ │ │ ├── radio-group
│ │ │ │ ├── index.ts
│ │ │ │ ├── radio-group.svelte
│ │ │ │ └── radio-group-item.svelte
│ │ │ ├── toggle-group
│ │ │ │ ├── index.ts
│ │ │ │ ├── toggle-group-item.svelte
│ │ │ │ └── toggle-group.svelte
│ │ │ ├── toggle
│ │ │ │ ├── index.ts
│ │ │ │ └── toggle.svelte
│ │ │ ├── form
│ │ │ │ ├── form-button.svelte
│ │ │ │ ├── form-legend.svelte
│ │ │ │ ├── form-description.svelte
│ │ │ │ ├── form-fieldset.svelte
│ │ │ │ ├── form-label.svelte
│ │ │ │ ├── form-field-errors.svelte
│ │ │ │ ├── index.ts
│ │ │ │ ├── form-field.svelte
│ │ │ │ └── form-element-field.svelte
│ │ │ ├── select
│ │ │ │ ├── select-group.svelte
│ │ │ │ ├── select-separator.svelte
│ │ │ │ ├── select-label.svelte
│ │ │ │ ├── select-group-heading.svelte
│ │ │ │ ├── select-scroll-up-button.svelte
│ │ │ │ ├── select-scroll-down-button.svelte
│ │ │ │ ├── index.ts
│ │ │ │ ├── select-item.svelte
│ │ │ │ ├── select-trigger.svelte
│ │ │ │ └── select-content.svelte
│ │ │ ├── sheet
│ │ │ │ ├── sheet-close.svelte
│ │ │ │ ├── sheet-trigger.svelte
│ │ │ │ ├── sheet-title.svelte
│ │ │ │ ├── sheet-description.svelte
│ │ │ │ ├── sheet-header.svelte
│ │ │ │ ├── sheet-footer.svelte
│ │ │ │ ├── sheet-overlay.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── sheet-content.svelte
│ │ │ ├── dialog
│ │ │ │ ├── dialog-close.svelte
│ │ │ │ ├── dialog-trigger.svelte
│ │ │ │ ├── dialog-title.svelte
│ │ │ │ ├── dialog-description.svelte
│ │ │ │ ├── dialog-overlay.svelte
│ │ │ │ ├── dialog-header.svelte
│ │ │ │ ├── dialog-footer.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── dialog-content.svelte
│ │ │ ├── tooltip
│ │ │ │ ├── tooltip-trigger.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── tooltip-content.svelte
│ │ │ ├── sidebar
│ │ │ │ ├── constants.ts
│ │ │ │ ├── sidebar-separator.svelte
│ │ │ │ ├── sidebar-input.svelte
│ │ │ │ ├── sidebar-footer.svelte
│ │ │ │ ├── sidebar-header.svelte
│ │ │ │ ├── sidebar-group-content.svelte
│ │ │ │ ├── sidebar-group.svelte
│ │ │ │ ├── sidebar-menu-item.svelte
│ │ │ │ ├── sidebar-menu-sub-item.svelte
│ │ │ │ ├── sidebar-menu.svelte
│ │ │ │ ├── sidebar-content.svelte
│ │ │ │ ├── sidebar-menu-sub.svelte
│ │ │ │ ├── sidebar-inset.svelte
│ │ │ │ ├── sidebar-trigger.svelte
│ │ │ │ ├── sidebar-menu-badge.svelte
│ │ │ │ ├── sidebar-menu-skeleton.svelte
│ │ │ │ ├── sidebar-group-label.svelte
│ │ │ │ ├── sidebar-group-action.svelte
│ │ │ │ ├── sidebar-rail.svelte
│ │ │ │ ├── sidebar-provider.svelte
│ │ │ │ ├── sidebar-menu-sub-button.svelte
│ │ │ │ ├── sidebar-menu-action.svelte
│ │ │ │ └── index.ts
│ │ │ ├── avatar
│ │ │ │ ├── index.ts
│ │ │ │ ├── avatar-image.svelte
│ │ │ │ ├── avatar-fallback.svelte
│ │ │ │ └── avatar.svelte
│ │ │ ├── dropdown-menu
│ │ │ │ ├── dropdown-menu-group.svelte
│ │ │ │ ├── dropdown-menu-trigger.svelte
│ │ │ │ ├── dropdown-menu-radio-group.svelte
│ │ │ │ ├── dropdown-menu-separator.svelte
│ │ │ │ ├── dropdown-menu-shortcut.svelte
│ │ │ │ ├── dropdown-menu-group-heading.svelte
│ │ │ │ ├── dropdown-menu-label.svelte
│ │ │ │ ├── dropdown-menu-sub-content.svelte
│ │ │ │ ├── dropdown-menu-sub-trigger.svelte
│ │ │ │ ├── dropdown-menu-content.svelte
│ │ │ │ ├── dropdown-menu-item.svelte
│ │ │ │ ├── dropdown-menu-radio-item.svelte
│ │ │ │ ├── dropdown-menu-checkbox-item.svelte
│ │ │ │ └── index.ts
│ │ │ ├── alert-dialog
│ │ │ │ ├── alert-dialog-trigger.svelte
│ │ │ │ ├── alert-dialog-title.svelte
│ │ │ │ ├── alert-dialog-description.svelte
│ │ │ │ ├── alert-dialog-action.svelte
│ │ │ │ ├── alert-dialog-cancel.svelte
│ │ │ │ ├── alert-dialog-overlay.svelte
│ │ │ │ ├── alert-dialog-header.svelte
│ │ │ │ ├── alert-dialog-footer.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── alert-dialog-content.svelte
│ │ │ ├── accordion
│ │ │ │ ├── accordion.svelte
│ │ │ │ ├── accordion-root.svelte
│ │ │ │ ├── index.ts
│ │ │ │ ├── accordion-item.svelte
│ │ │ │ ├── accordion-content.svelte
│ │ │ │ └── accordion-trigger.svelte
│ │ │ ├── button
│ │ │ │ └── index.ts
│ │ │ ├── tabs
│ │ │ │ ├── index.ts
│ │ │ │ ├── tabs-content.svelte
│ │ │ │ ├── tabs.svelte
│ │ │ │ ├── tabs-list.svelte
│ │ │ │ └── tabs-trigger.svelte
│ │ │ ├── alert
│ │ │ │ ├── index.ts
│ │ │ │ ├── alert-title.svelte
│ │ │ │ ├── alert-description.svelte
│ │ │ │ └── alert.svelte
│ │ │ ├── popover
│ │ │ │ ├── popover-trigger.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── popover-content.svelte
│ │ │ ├── breadcrumb
│ │ │ │ ├── breadcrumb.svelte
│ │ │ │ ├── breadcrumb-item.svelte
│ │ │ │ ├── breadcrumb-list.svelte
│ │ │ │ ├── breadcrumb-page.svelte
│ │ │ │ ├── index.ts
│ │ │ │ ├── breadcrumb-separator.svelte
│ │ │ │ ├── breadcrumb-ellipsis.svelte
│ │ │ │ └── breadcrumb-link.svelte
│ │ │ ├── card
│ │ │ │ ├── card-content.svelte
│ │ │ │ ├── card-title.svelte
│ │ │ │ ├── card-description.svelte
│ │ │ │ ├── card-footer.svelte
│ │ │ │ ├── card-action.svelte
│ │ │ │ ├── card.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── card-header.svelte
│ │ │ └── table
│ │ │ │ ├── table-header.svelte
│ │ │ │ ├── table-body.svelte
│ │ │ │ ├── table-cell.svelte
│ │ │ │ ├── table-caption.svelte
│ │ │ │ ├── table-footer.svelte
│ │ │ │ ├── table.svelte
│ │ │ │ ├── table-head.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── table-row.svelte
│ │ ├── placeholder-pattern.svelte
│ │ ├── app-sidebar-header.svelte
│ │ ├── app-sidebar.svelte
│ │ ├── settings-navbar.svelte
│ │ ├── app-head.svelte
│ │ ├── app-sidebar-content.svelte
│ │ └── theme-switch.svelte
│ ├── utils
│ │ ├── themes.ts
│ │ ├── mail
│ │ │ ├── types.ts
│ │ │ └── templates
│ │ │ │ ├── mail-theme.ts
│ │ │ │ ├── welcome.svelte
│ │ │ │ └── reset-password.svelte
│ │ ├── helpers
│ │ │ ├── name.ts
│ │ │ ├── image.ts
│ │ │ └── generate.ts
│ │ ├── utils.ts
│ │ └── messages.json
│ ├── assets
│ │ ├── logo.png
│ │ ├── avatar.png
│ │ └── meta_image.png
│ ├── db
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── session.ts
│ │ │ ├── verification.ts
│ │ │ ├── account.ts
│ │ │ └── user.ts
│ │ ├── migrations
│ │ │ ├── meta
│ │ │ │ └── _journal.json
│ │ │ └── 0000_tricky_angel.sql
│ │ └── clear.ts
│ ├── hooks
│ │ ├── is-mobile.svelte.ts
│ │ └── use-theme.ts
│ ├── server
│ │ ├── storage.ts
│ │ └── database.ts
│ └── validations
│ │ └── files.ts
├── routes
│ ├── +page.server.ts
│ ├── (app)
│ │ ├── +layout.server.ts
│ │ ├── dashboard
│ │ │ └── +page.svelte
│ │ └── settings
│ │ │ └── +layout.svelte
│ ├── +layout.server.ts
│ ├── +error.svelte
│ ├── (auth)
│ │ ├── login
│ │ │ ├── google
│ │ │ │ └── +server.ts
│ │ │ └── +page.server.ts
│ │ ├── +layout.svelte
│ │ ├── logout
│ │ │ └── +server.ts
│ │ └── password
│ │ │ ├── +page.server.ts
│ │ │ ├── +page.svelte
│ │ │ └── reset
│ │ │ ├── +page.server.ts
│ │ │ └── +page.svelte
│ ├── api
│ │ └── upload
│ │ │ └── +server.ts
│ └── +layout.svelte
├── hooks.server.ts
├── app.d.ts
└── app.html
├── tests
├── unit
│ └── demo.spec.ts
└── e2e
│ └── homepage.test.ts
├── playwright.config.ts
├── tsconfig.json
├── components.json
├── drizzle.config.ts
├── .env.example
├── svelte.config.js
├── vite.config.ts
├── .prettierrc
├── .gitignore
├── .prettierignore
├── eslint.config.js
└── .github
└── workflows
└── ci.yml
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /admin
--------------------------------------------------------------------------------
/src/lib/components/ui/sonner/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Toaster } from './sonner.svelte';
2 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/static/favicon.png
--------------------------------------------------------------------------------
/src/lib/utils/themes.ts:
--------------------------------------------------------------------------------
1 | export const themes = { default: 'dark', light: 'light', dark: 'dark' };
2 |
--------------------------------------------------------------------------------
/src/lib/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/src/lib/assets/logo.png
--------------------------------------------------------------------------------
/src/lib/assets/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/src/lib/assets/avatar.png
--------------------------------------------------------------------------------
/src/lib/assets/meta_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/src/lib/assets/meta_image.png
--------------------------------------------------------------------------------
/static/fonts/Sora-VariableFont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/static/fonts/Sora-VariableFont.ttf
--------------------------------------------------------------------------------
/src/lib/utils/mail/types.ts:
--------------------------------------------------------------------------------
1 | export interface EmailTemplate {
2 | subject: string;
3 | html: string;
4 | text: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/components/ui/input/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './input.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Input
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/label/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './label.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Label
7 | };
8 |
--------------------------------------------------------------------------------
/static/fonts/Montserrat-VariableFont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n00ki/sveltekit-omakase/HEAD/static/fonts/Montserrat-VariableFont.ttf
--------------------------------------------------------------------------------
/src/lib/components/ui/slider/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './slider.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Slider
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/switch/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './switch.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Switch
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/checkbox/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './checkbox.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Checkbox
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/progress/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './progress.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Progress
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/separator/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './separator.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Separator
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/skeleton/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './skeleton.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Skeleton
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/textarea/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './textarea.svelte';
2 |
3 | export {
4 | Root,
5 | //
6 | Root as Textarea
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/db/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './account';
2 | export * from './session';
3 | export * from './user';
4 | export * from './verification';
5 |
--------------------------------------------------------------------------------
/src/lib/components/ui/badge/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Badge } from './badge.svelte';
2 | export { badgeVariants, type BadgeVariant } from './badge.svelte';
3 |
--------------------------------------------------------------------------------
/src/routes/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { redirectIfLoggedIn } from '$lib/server/auth';
2 |
3 | export async function load() {
4 | redirectIfLoggedIn();
5 | }
6 |
--------------------------------------------------------------------------------
/tests/unit/demo.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 |
3 | describe('sum test', () => {
4 | it('adds 1 + 2 to equal 3', () => {
5 | expect(1 + 2).toBe(3);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import { requireLogin } from '$lib/server/auth';
2 |
3 | export async function load() {
4 | const { user, session } = requireLogin();
5 | return { user, session };
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/components/ui/kbd/index.ts:
--------------------------------------------------------------------------------
1 | import Group from './kbd-group.svelte';
2 | import Root from './kbd.svelte';
3 |
4 | export {
5 | Root,
6 | Group,
7 | //
8 | Root as Kbd,
9 | Group as KbdGroup
10 | };
11 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@playwright/test';
2 |
3 | export default defineConfig({
4 | webServer: {
5 | command: 'vite build && vite preview',
6 | port: 4173
7 | },
8 | testDir: 'tests/e2e'
9 | });
10 |
--------------------------------------------------------------------------------
/src/lib/components/ui/radio-group/index.ts:
--------------------------------------------------------------------------------
1 | import Item from './radio-group-item.svelte';
2 | import Root from './radio-group.svelte';
3 |
4 | export {
5 | Root,
6 | Item,
7 | //
8 | Root as RadioGroup,
9 | Item as RadioGroupItem
10 | };
11 |
--------------------------------------------------------------------------------
/src/lib/components/ui/toggle-group/index.ts:
--------------------------------------------------------------------------------
1 | import Item from './toggle-group-item.svelte';
2 | import Root from './toggle-group.svelte';
3 |
4 | export {
5 | Root,
6 | Item,
7 | //
8 | Root as ToggleGroup,
9 | Item as ToggleGroupItem
10 | };
11 |
--------------------------------------------------------------------------------
/src/lib/components/ui/toggle/index.ts:
--------------------------------------------------------------------------------
1 | import Root from './toggle.svelte';
2 |
3 | export { toggleVariants, type ToggleSize, type ToggleVariant, type ToggleVariants } from './toggle.svelte';
4 |
5 | export {
6 | Root,
7 | //
8 | Root as Toggle
9 | };
10 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-button.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/db/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7",
3 | "dialect": "sqlite",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "6",
8 | "when": 1754650179463,
9 | "tag": "0000_tricky_angel",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-group.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-close.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-close.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-trigger.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/hooks/is-mobile.svelte.ts:
--------------------------------------------------------------------------------
1 | import { MediaQuery } from 'svelte/reactivity';
2 |
3 | const DEFAULT_MOBILE_BREAKPOINT = 768;
4 |
5 | export class IsMobile extends MediaQuery {
6 | constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
7 | super(`max-width: ${breakpoint - 1}px`);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-trigger.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tooltip/tooltip-trigger.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/routes/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import { loadFlash } from 'sveltekit-flash-message/server';
2 |
3 | import { auth } from '$lib/server/auth';
4 |
5 | export const load = loadFlash(async ({ request }) => {
6 | const session = await auth.api.getSession(request);
7 | return {
8 | user: session?.user ?? null
9 | };
10 | });
11 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/constants.ts:
--------------------------------------------------------------------------------
1 | export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
2 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
3 | export const SIDEBAR_WIDTH = '16rem';
4 | export const SIDEBAR_WIDTH_MOBILE = '18rem';
5 | export const SIDEBAR_WIDTH_ICON = '3rem';
6 | export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
7 |
--------------------------------------------------------------------------------
/src/lib/components/ui/avatar/index.ts:
--------------------------------------------------------------------------------
1 | import Fallback from './avatar-fallback.svelte';
2 | import Image from './avatar-image.svelte';
3 | import Root from './avatar.svelte';
4 |
5 | export {
6 | Root,
7 | Image,
8 | Fallback,
9 | //
10 | Root as Avatar,
11 | Image as AvatarImage,
12 | Fallback as AvatarFallback
13 | };
14 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/accordion/accordion.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/accordion/accordion-root.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/button/index.ts:
--------------------------------------------------------------------------------
1 | import type { ButtonProps, ButtonSize, ButtonVariant } from './button.svelte';
2 |
3 | import Root, { buttonVariants } from './button.svelte';
4 |
5 | export {
6 | Root,
7 | type ButtonProps as Props,
8 | //
9 | Root as Button,
10 | buttonVariants,
11 | type ButtonProps,
12 | type ButtonSize,
13 | type ButtonVariant
14 | };
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tabs/index.ts:
--------------------------------------------------------------------------------
1 | import Content from './tabs-content.svelte';
2 | import List from './tabs-list.svelte';
3 | import Trigger from './tabs-trigger.svelte';
4 | import Root from './tabs.svelte';
5 |
6 | export {
7 | Root,
8 | Content,
9 | List,
10 | Trigger,
11 | //
12 | Root as Tabs,
13 | Content as TabsContent,
14 | List as TabsList,
15 | Trigger as TabsTrigger
16 | };
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert/index.ts:
--------------------------------------------------------------------------------
1 | import Description from './alert-description.svelte';
2 | import Title from './alert-title.svelte';
3 | import Root from './alert.svelte';
4 |
5 | export { alertVariants, type AlertVariant } from './alert.svelte';
6 |
7 | export {
8 | Root,
9 | Description,
10 | Title,
11 | //
12 | Root as Alert,
13 | Description as AlertDescription,
14 | Title as AlertTitle
15 | };
16 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tabs/tabs-content.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/lib/components/ui/popover/popover-trigger.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/+error.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | {page.status}: {page.error?.message}
8 |
9 |
10 | Go back home
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/components/ui/kbd/kbd-group.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {@render children?.()}
11 |
12 |
--------------------------------------------------------------------------------
/src/lib/components/ui/accordion/index.ts:
--------------------------------------------------------------------------------
1 | import Content from './accordion-content.svelte';
2 | import Item from './accordion-item.svelte';
3 | import Trigger from './accordion-trigger.svelte';
4 | import Root from './accordion.svelte';
5 |
6 | export {
7 | Root,
8 | Content,
9 | Item,
10 | Trigger,
11 | //
12 | Root as Accordion,
13 | Content as AccordionContent,
14 | Item as AccordionItem,
15 | Trigger as AccordionTrigger
16 | };
17 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://shadcn-svelte.com/schema.json",
3 | "tailwind": {
4 | "css": "src/styles/app.css",
5 | "baseColor": "stone"
6 | },
7 | "aliases": {
8 | "components": "$components",
9 | "utils": "$lib/utils/utils",
10 | "ui": "$lib/components/ui",
11 | "hooks": "$lib/hooks",
12 | "lib": "$lib"
13 | },
14 | "typescript": true,
15 | "registry": "https://shadcn-svelte.com/registry"
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/avatar/avatar-image.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-title.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/server/storage.ts:
--------------------------------------------------------------------------------
1 | import { R2_ACCESS_KEY_ID, R2_ACCOUNT_ID, R2_SECRET_ACCESS_KEY } from '$env/static/private';
2 |
3 | import { S3Client } from '@aws-sdk/client-s3';
4 |
5 | const r2Endpoint = `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`;
6 |
7 | export const s3 = new S3Client({
8 | endpoint: r2Endpoint,
9 | credentials: {
10 | accessKeyId: R2_ACCESS_KEY_ID,
11 | secretAccessKey: R2_SECRET_ACCESS_KEY
12 | },
13 | region: 'auto'
14 | });
15 |
--------------------------------------------------------------------------------
/src/lib/utils/mail/templates/mail-theme.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from 'sailkit';
2 |
3 | export const mailTheme = createTheme({
4 | fonts: [
5 | {
6 | name: 'Outfit',
7 | href: 'https://fonts.googleapis.com/css2?family=Outfit'
8 | }
9 | ],
10 | styles: {
11 | global: {
12 | fontFamily:
13 | 'Outfit, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Helvetica, Arial, sans-serif'
14 | }
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-title.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tabs/tabs.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/accordion/accordion-item.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-description.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-description.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'drizzle-kit';
2 |
3 | import 'dotenv/config';
4 |
5 | export default defineConfig({
6 | dialect: 'turso',
7 | schema: './src/lib/db/models/index.ts',
8 | out: './src/lib/db/migrations',
9 | breakpoints: true,
10 | casing: 'snake_case',
11 | strict: true,
12 | dbCredentials: {
13 | url: process.env.DATABASE_URL ?? '',
14 | authToken: process.env.NODE_ENV === 'development' ? undefined : process.env.DATABASE_AUTH_TOKEN
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | import { building } from '$app/environment';
2 |
3 | import { svelteKitHandler } from 'better-auth/svelte-kit';
4 |
5 | import { auth } from '$lib/server/auth';
6 |
7 | export async function handle({ event, resolve }) {
8 | const session = await auth.api.getSession(event.request);
9 | if (session) {
10 | event.locals.session = session.session;
11 | event.locals.user = session.user;
12 | }
13 |
14 | return svelteKitHandler({ event, resolve, auth, building });
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/popover/index.ts:
--------------------------------------------------------------------------------
1 | import { Popover as PopoverPrimitive } from 'bits-ui';
2 |
3 | import Content from './popover-content.svelte';
4 | import Trigger from './popover-trigger.svelte';
5 |
6 | const Root = PopoverPrimitive.Root;
7 | const Close = PopoverPrimitive.Close;
8 |
9 | export {
10 | Root,
11 | Content,
12 | Trigger,
13 | Close,
14 | //
15 | Root as Popover,
16 | Content as PopoverContent,
17 | Trigger as PopoverTrigger,
18 | Close as PopoverClose
19 | };
20 |
--------------------------------------------------------------------------------
/src/lib/utils/helpers/name.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Extracts user initials for avatar placeholders and compact displays.
3 | * Provides a consistent way to represent users when profile images aren't available.
4 | *
5 | * @param firstName - User's first name
6 | * @param lastName - User's last name
7 | * @returns Two-character initials in uppercase
8 | */
9 | export function getInitials(firstName: string, lastName: string): string {
10 | return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
11 | }
12 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # General
2 | PUBLIC_BASE_URL=http://localhost:5173
3 |
4 | # Database
5 | DATABASE_URL=file:sveltekit_omakase_local.db
6 |
7 | # Better-Auth
8 | BETTER_AUTH_URL=
9 | BETTER_AUTH_SECRET=
10 |
11 | # OAuth
12 | GOOGLE_CLIENT_ID=
13 | GOOGLE_CLIENT_SECRET=
14 |
15 | # R2
16 | R2_ACCOUNT_ID=
17 | R2_ACCESS_KEY_ID=
18 | R2_SECRET_ACCESS_KEY=
19 | PUBLIC_R2_BUCKET_NAME=
20 | PUBLIC_R2_BUCKET_URL= # use R2.dev subdomain in development
21 |
22 | # Resend
23 | RESEND_API_KEY=
24 | EMAIL_SENDER=
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/avatar/avatar-fallback.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/breadcrumb.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
16 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-legend.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
16 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sonner/sonner.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
16 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tabs/tabs-list.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/radio-group/radio-group.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-separator.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-separator.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
16 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-content.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace App {
3 | interface Locals {
4 | user: import('$lib/server/auth').User;
5 | session: import('$lib/server/auth').Session;
6 | }
7 |
8 | interface PageData {
9 | user: User | null;
10 | session: Session | null;
11 | metadata: {
12 | title: string;
13 | description: string;
14 | image: string;
15 | url: string;
16 | breadcrumbs: {
17 | title: string;
18 | href: string;
19 | }[];
20 | };
21 | }
22 | }
23 | }
24 |
25 | export {};
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
16 |
--------------------------------------------------------------------------------
/src/lib/components/ui/avatar/avatar.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-description.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 |
--------------------------------------------------------------------------------
/src/lib/server/database.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import type { Client } from '@libsql/client';
4 |
5 | import { createClient } from '@libsql/client';
6 | import { drizzle } from 'drizzle-orm/libsql';
7 |
8 | import * as schema from '$lib/db/models/index';
9 |
10 | export const client: Client = createClient({
11 | url: process.env.DATABASE_URL || '',
12 | authToken: process.env.NODE_ENV === 'development' ? undefined : process.env.DATABASE_AUTH_TOKEN
13 | });
14 |
15 | const db = drizzle(client, {
16 | schema,
17 | casing: 'snake_case'
18 | });
19 |
20 | export default db;
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-title.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-header.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table-header.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tooltip/index.ts:
--------------------------------------------------------------------------------
1 | import { Tooltip as TooltipPrimitive } from 'bits-ui';
2 |
3 | import Content from './tooltip-content.svelte';
4 | import Trigger from './tooltip-trigger.svelte';
5 |
6 | const Root = TooltipPrimitive.Root;
7 | const Provider = TooltipPrimitive.Provider;
8 | const Portal = TooltipPrimitive.Portal;
9 |
10 | export {
11 | Root,
12 | Trigger,
13 | Content,
14 | Provider,
15 | Portal,
16 | //
17 | Root as Tooltip,
18 | Content as TooltipContent,
19 | Trigger as TooltipTrigger,
20 | Provider as TooltipProvider,
21 | Portal as TooltipPortal
22 | };
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-footer.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-description.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table-body.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-overlay.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/skeleton/skeleton.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-overlay.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/db/models/session.ts:
--------------------------------------------------------------------------------
1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2 |
3 | import { User } from './user';
4 |
5 | export const Session = sqliteTable('session', {
6 | id: integer().primaryKey({ autoIncrement: true }),
7 | token: text().notNull().unique(),
8 | createdAt: integer({ mode: 'timestamp' }).notNull(),
9 | updatedAt: integer({ mode: 'timestamp' }).notNull(),
10 | expiresAt: integer({ mode: 'timestamp' }).notNull(),
11 | ipAddress: text(),
12 | userAgent: text(),
13 | userId: text()
14 | .notNull()
15 | .references(() => User.id, { onDelete: 'cascade' })
16 | });
17 |
--------------------------------------------------------------------------------
/src/lib/components/placeholder-pattern.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table-cell.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
16 | {@render children?.()}
17 | |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/separator/separator.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-input.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-footer.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
16 | {@render children?.()}
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table-caption.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | kit: {
7 | adapter: adapter(),
8 |
9 | alias: {
10 | $components: 'src/lib/components',
11 | '$components/*': 'src/lib/components/*',
12 |
13 | $models: 'src/lib/db/models',
14 | '$models/*': 'src/lib/db/models/*',
15 |
16 | $queries: 'src/lib/db/queries',
17 | '$queries/*': 'src/lib/db/queries/*'
18 | }
19 | },
20 |
21 | preprocess: vitePreprocess()
22 | };
23 |
24 | export default config;
25 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-header.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/label/label.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-label.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/db/models/verification.ts:
--------------------------------------------------------------------------------
1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2 |
3 | export const Verification = sqliteTable('verification', {
4 | id: integer().primaryKey({ autoIncrement: true }),
5 | identifier: text().notNull(),
6 | value: text().notNull(),
7 | expiresAt: integer().notNull(),
8 | createdAt: integer({ mode: 'timestamp' })
9 | .notNull()
10 | .$defaultFn(() => new Date()),
11 | updatedAt: integer({ mode: 'timestamp' })
12 | .notNull()
13 | .$defaultFn(() => new Date())
14 | .$onUpdateFn(() => new Date())
15 | });
16 |
17 | export type verification = typeof Verification.$inferSelect;
18 |
--------------------------------------------------------------------------------
/src/routes/(auth)/login/google/+server.ts:
--------------------------------------------------------------------------------
1 | import { auth } from '$lib/server/auth';
2 |
3 | export async function GET(): Promise {
4 | try {
5 | const response = await auth.api.signInSocial({
6 | body: {
7 | provider: 'google'
8 | }
9 | });
10 |
11 | return new Response(null, {
12 | status: 302,
13 | headers: {
14 | Location: response.url?.toString() ?? '/login'
15 | }
16 | });
17 | } catch (error) {
18 | console.error(error);
19 | return new Response(null, {
20 | status: 500,
21 | headers: {
22 | Location: '/login'
23 | }
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-footer.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-header.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert/alert-title.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-action.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-footer.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-fieldset.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group-content.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table-footer.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | tr]:last:border-b-0', className)}
19 | {...restProps}
20 | >
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-item.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 | {@render children?.()}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 | {@render children?.()}
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import tailwindcss from '@tailwindcss/vite';
3 | import { defineConfig } from 'vitest/config';
4 |
5 | export default defineConfig({
6 | plugins: [tailwindcss(), sveltekit()],
7 | test: {
8 | expect: { requireAssertions: true },
9 | projects: [
10 | {
11 | extends: './vite.config.ts',
12 | test: {
13 | name: 'server',
14 | environment: 'node',
15 | include: ['src/**/*.{test,spec}.{js,ts}', 'tests/unit/**/*.{test,spec}.{js,ts}'],
16 | exclude: ['src/**/*.svelte.{test,spec}.{js,ts}', 'tests/e2e/**']
17 | }
18 | }
19 | ]
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/index.ts:
--------------------------------------------------------------------------------
1 | import Action from './card-action.svelte';
2 | import Content from './card-content.svelte';
3 | import Description from './card-description.svelte';
4 | import Footer from './card-footer.svelte';
5 | import Header from './card-header.svelte';
6 | import Title from './card-title.svelte';
7 | import Root from './card.svelte';
8 |
9 | export {
10 | Root,
11 | Content,
12 | Description,
13 | Footer,
14 | Header,
15 | Title,
16 | Action,
17 | //
18 | Root as Card,
19 | Content as CardContent,
20 | Description as CardDescription,
21 | Footer as CardFooter,
22 | Header as CardHeader,
23 | Title as CardTitle,
24 | Action as CardAction
25 | };
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table-head.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
19 | {@render children?.()}
20 | |
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-group-heading.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/src/routes/(app)/dashboard/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {#each Array(3)}
7 |
10 | {/each}
11 |
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
24 | {@render children?.()}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert/alert-description.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
24 | {@render children?.()}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/index.ts:
--------------------------------------------------------------------------------
1 | import Body from './table-body.svelte';
2 | import Caption from './table-caption.svelte';
3 | import Cell from './table-cell.svelte';
4 | import Footer from './table-footer.svelte';
5 | import Head from './table-head.svelte';
6 | import Header from './table-header.svelte';
7 | import Row from './table-row.svelte';
8 | import Root from './table.svelte';
9 |
10 | export {
11 | Root,
12 | Body,
13 | Caption,
14 | Cell,
15 | Footer,
16 | Head,
17 | Header,
18 | Row,
19 | //
20 | Root as Table,
21 | Body as TableBody,
22 | Caption as TableCaption,
23 | Cell as TableCell,
24 | Footer as TableFooter,
25 | Head as TableHead,
26 | Header as TableHeader,
27 | Row as TableRow
28 | };
29 |
--------------------------------------------------------------------------------
/src/lib/components/ui/table/table-row.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | svelte-css-wrapper]:[&>th,td]:bg-muted/50',
20 | className
21 | )}
22 | {...restProps}
23 | >
24 | {@render children?.()}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/index.ts:
--------------------------------------------------------------------------------
1 | import Ellipsis from './breadcrumb-ellipsis.svelte';
2 | import Item from './breadcrumb-item.svelte';
3 | import Link from './breadcrumb-link.svelte';
4 | import List from './breadcrumb-list.svelte';
5 | import Page from './breadcrumb-page.svelte';
6 | import Separator from './breadcrumb-separator.svelte';
7 | import Root from './breadcrumb.svelte';
8 |
9 | export {
10 | Root,
11 | Ellipsis,
12 | Item,
13 | Separator,
14 | Link,
15 | List,
16 | Page,
17 | //
18 | Root as Breadcrumb,
19 | Ellipsis as BreadcrumbEllipsis,
20 | Item as BreadcrumbItem,
21 | Separator as BreadcrumbSeparator,
22 | Link as BreadcrumbLink,
23 | List as BreadcrumbList,
24 | Page as BreadcrumbPage
25 | };
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
25 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
17 | %sveltekit.head%
18 |
19 |
20 | %sveltekit.body%
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
25 | {@render children?.()}
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/utils/helpers/image.ts:
--------------------------------------------------------------------------------
1 | import { PUBLIC_R2_BUCKET_URL } from '$env/static/public';
2 |
3 | import avatarPlaceholder from '$lib/assets/avatar.png';
4 |
5 | /**
6 | * Resolves avatar URLs with automatic fallback to placeholder.
7 | * Abstracts the complexity of handling both missing avatars and R2 storage paths,
8 | * ensuring consistent avatar display across the application.
9 | *
10 | * @param image - Avatar filename or null/undefined
11 | * @returns Complete avatar URL or placeholder path
12 | */
13 | export function getAvatarUrl(image: string | null | undefined): string {
14 | if (!image) return avatarPlaceholder;
15 |
16 | // Construct the R2 URL for the local filename
17 | return `${PUBLIC_R2_BUCKET_URL}/images/avatars/${image}`;
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-content.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
25 | {@render children?.()}
26 |
27 |
--------------------------------------------------------------------------------
/tests/e2e/homepage.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test('home page has expected h1', async ({ page }) => {
4 | await page.goto('/');
5 | await expect(page.locator('h1')).toHaveText('SvelteKit Omakase');
6 | });
7 |
8 | test('home page has navigation links', async ({ page }) => {
9 | await page.goto('/');
10 |
11 | // Check for Login button
12 | const loginButton = page.locator('a[href="/login"]');
13 | await expect(loginButton).toBeVisible();
14 | await expect(loginButton).toHaveText('Login');
15 |
16 | // Check for Register button
17 | const registerButton = page.locator('a[href="/register"]');
18 | await expect(registerButton).toBeVisible();
19 | await expect(registerButton).toHaveText('Register');
20 | });
21 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-label.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 | {#snippet child({ props })}
20 |
23 | {/snippet}
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/ui/card/card-header.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
24 | {@render children?.()}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 | svg]:size-3.5', className)}
18 | {...restProps}
19 | >
20 | {#if children}
21 | {@render children?.()}
22 | {:else}
23 |
24 | {/if}
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-scroll-up-button.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/db/models/account.ts:
--------------------------------------------------------------------------------
1 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2 |
3 | import { User } from './user';
4 |
5 | export const Account = sqliteTable('account', {
6 | id: integer().primaryKey({ autoIncrement: true }),
7 | accountId: text().notNull(),
8 | providerId: text().notNull(),
9 | userId: text()
10 | .notNull()
11 | .references(() => User.id, { onDelete: 'cascade' }),
12 | accessToken: text(),
13 | refreshToken: text(),
14 | idToken: text(),
15 | accessTokenExpiresAt: integer(),
16 | refreshTokenExpiresAt: integer(),
17 | scope: text(),
18 | password: text(),
19 | createdAt: integer({ mode: 'timestamp' }).notNull(),
20 | updatedAt: integer({ mode: 'timestamp' }).notNull()
21 | });
22 |
23 | export type Account = typeof Account.$inferSelect;
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/accordion/accordion-content.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
22 |
23 | {@render children?.()}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
26 | {@render children?.()}
27 |
28 |
--------------------------------------------------------------------------------
/src/lib/components/ui/kbd/kbd.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
19 | {@render children?.()}
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from 'clsx';
2 |
3 | import { clsx } from 'clsx';
4 | import { twMerge } from 'tailwind-merge';
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs));
8 | }
9 |
10 | export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
11 |
12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
13 | export type WithoutChild = T extends { child?: any } ? Omit : T;
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | export type WithoutChildren = T extends { children?: any } ? Omit : T;
16 | export type WithoutChildrenOrChild = WithoutChildren>;
17 | export type WithElementRef = T & { ref?: U | null };
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-scroll-down-button.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/utils/helpers/generate.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from 'nanoid';
2 |
3 | /** Alphanumeric characters used for ID generation (lowercase, URL-safe) */
4 | const ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
5 |
6 | /** Standard length for generated IDs across the application */
7 | const ID_LENGTH = 12;
8 |
9 | /**
10 | * Generates consistent, URL-safe IDs for database records and file names.
11 | * Uses a curated alphabet to ensure compatibility across systems while maintaining
12 | * sufficient entropy for uniqueness.
13 | *
14 | * @returns A 12-character alphanumeric ID
15 | * @example
16 | * ```ts
17 | * const userId = generateNanoId(); // "a1b2c3d4e5f6"
18 | * ```
19 | */
20 | export function generateNanoId() {
21 | const nanoid = customAlphabet(ID_ALPHABET, ID_LENGTH);
22 | return nanoid();
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
24 |
25 | More
26 |
27 |
--------------------------------------------------------------------------------
/src/routes/(auth)/+layout.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
17 |
18 |
19 |
20 |
21 | {@render children?.()}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/routes/(auth)/logout/+server.ts:
--------------------------------------------------------------------------------
1 | import type { RequestHandler } from '@sveltejs/kit';
2 |
3 | import { redirect } from 'sveltekit-flash-message/server';
4 |
5 | import { auth, requireLogin } from '$lib/server/auth';
6 | import * as m from '$lib/utils/messages.json';
7 |
8 | export const POST: RequestHandler = async (event) => {
9 | requireLogin();
10 |
11 | try {
12 | await auth.api.signOut({
13 | headers: event.request.headers
14 | });
15 | } catch (error) {
16 | console.log(error);
17 | redirect(
18 | '/',
19 | {
20 | status: 500,
21 | type: 'error',
22 | message: m.general.error
23 | },
24 | event
25 | );
26 | }
27 | redirect(
28 | '/',
29 | {
30 | status: 303,
31 | type: 'success',
32 | message: m.auth.logout.success
33 | },
34 | event
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/lib/components/app-sidebar-header.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | {#snippet child({ props })}
11 |
12 |
17 |
18 | SvelteKit Omakase
19 |
20 |
21 | {/snippet}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-inset.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
25 | {@render children?.()}
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/ui/progress/progress.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
25 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 | {#if child}
28 | {@render child({ props: attrs })}
29 | {:else}
30 |
31 | {@render children?.()}
32 |
33 | {/if}
34 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 120,
6 | "plugins": ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7 | "importOrder": [
8 | "^\\$env/(.*)$",
9 | "",
10 | "",
11 | "^@lucide/svelte$",
12 | "",
13 | "^\\$app/(.*)$",
14 | "",
15 | "",
16 | "",
17 | "",
18 | "^\\$lib/(?!assets/)(.*)$",
19 | "",
20 | "^\\$components/(.*)$",
21 | "",
22 | "^@lucide/svelte$",
23 | "^\\$lib/assets/(.*)$",
24 | "",
25 | "^[./]"
26 | ],
27 | "importOrderParserPlugins": ["typescript", "decorators-legacy"],
28 | "importOrderTypeScriptVersion": "5.0.0",
29 | "overrides": [
30 | {
31 | "files": "*.svelte",
32 | "options": { "parser": "svelte" }
33 | }
34 | ],
35 | "tailwindStylesheet": "./src/styles/app.css"
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-field-errors.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 | {#snippet children({ errors, errorProps })}
21 | {#if childrenProp}
22 | {@render childrenProp({ errors, errorProps })}
23 | {:else}
24 | {#each errors as error (error)}
25 | {error}
26 | {/each}
27 | {/if}
28 | {/snippet}
29 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/index.ts:
--------------------------------------------------------------------------------
1 | import * as FormPrimitive from 'formsnap';
2 |
3 | import Button from './form-button.svelte';
4 | import Description from './form-description.svelte';
5 | import ElementField from './form-element-field.svelte';
6 | import FieldErrors from './form-field-errors.svelte';
7 | import Field from './form-field.svelte';
8 | import Fieldset from './form-fieldset.svelte';
9 | import Label from './form-label.svelte';
10 | import Legend from './form-legend.svelte';
11 |
12 | const Control = FormPrimitive.Control;
13 |
14 | export {
15 | Field,
16 | Control,
17 | Label,
18 | Button,
19 | FieldErrors,
20 | Description,
21 | Fieldset,
22 | Legend,
23 | ElementField,
24 | //
25 | Field as FormField,
26 | Control as FormControl,
27 | Description as FormDescription,
28 | Label as FormLabel,
29 | FieldErrors as FormFieldErrors,
30 | Fieldset as FormFieldset,
31 | Legend as FormLegend,
32 | ElementField as FormElementField,
33 | Button as FormButton
34 | };
35 |
--------------------------------------------------------------------------------
/src/lib/db/models/user.ts:
--------------------------------------------------------------------------------
1 | import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
2 |
3 | import { generateNanoId } from '../../utils/helpers/generate';
4 |
5 | export const User = sqliteTable(
6 | 'user',
7 | {
8 | id: integer().primaryKey({ autoIncrement: true }),
9 | publicId: text()
10 | .$defaultFn(() => generateNanoId())
11 | .unique(),
12 | email: text().notNull().unique(),
13 | emailVerified: integer({ mode: 'boolean' }).notNull().default(false),
14 | firstName: text().notNull(),
15 | lastName: text().notNull(),
16 | name: text().notNull(),
17 | avatar: text(),
18 | image: text(),
19 | createdAt: integer({ mode: 'timestamp' })
20 | .notNull()
21 | .$defaultFn(() => new Date()),
22 | updatedAt: integer({ mode: 'timestamp' })
23 | .notNull()
24 | .$defaultFn(() => new Date())
25 | .$onUpdateFn(() => new Date())
26 | },
27 | (User) => [uniqueIndex('public_id_index').on(User.publicId)]
28 | );
29 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-trigger.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
40 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/index.ts:
--------------------------------------------------------------------------------
1 | import { Dialog as SheetPrimitive } from 'bits-ui';
2 |
3 | import Close from './sheet-close.svelte';
4 | import Content from './sheet-content.svelte';
5 | import Description from './sheet-description.svelte';
6 | import Footer from './sheet-footer.svelte';
7 | import Header from './sheet-header.svelte';
8 | import Overlay from './sheet-overlay.svelte';
9 | import Title from './sheet-title.svelte';
10 | import Trigger from './sheet-trigger.svelte';
11 |
12 | const Root = SheetPrimitive.Root;
13 | const Portal = SheetPrimitive.Portal;
14 |
15 | export {
16 | Root,
17 | Close,
18 | Trigger,
19 | Portal,
20 | Overlay,
21 | Content,
22 | Header,
23 | Footer,
24 | Title,
25 | Description,
26 | //
27 | Root as Sheet,
28 | Close as SheetClose,
29 | Trigger as SheetTrigger,
30 | Portal as SheetPortal,
31 | Overlay as SheetOverlay,
32 | Content as SheetContent,
33 | Header as SheetHeader,
34 | Footer as SheetFooter,
35 | Title as SheetTitle,
36 | Description as SheetDescription
37 | };
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # -------------------------
2 | # OS-specific files
3 | # -------------------------
4 | .DS_Store
5 | Thumbs.db
6 |
7 | # -------------------------
8 | # Node.js dependencies
9 | # -------------------------
10 | node_modules/
11 |
12 | # -------------------------
13 | # Package manager lockfiles
14 | # -------------------------
15 | # package-lock.json
16 | # pnpm-lock.yaml
17 | # yarn.lock
18 | # bun.lockb
19 | # bun.lock
20 |
21 | # -------------------------
22 | # Build output
23 | # -------------------------
24 | /build
25 | /.svelte-kit
26 | /package
27 | /dist
28 |
29 | # -------------------------
30 | # Environment & secrets
31 | # -------------------------
32 | .env
33 | .env.*
34 | !.env.example
35 |
36 | # -------------------------
37 | # Databases & local data
38 | # -------------------------
39 | dev.db
40 | *.db
41 |
42 | # -------------------------
43 | # Test results
44 | # -------------------------
45 | /test-results
46 |
47 | # -------------------------
48 | # Editor-specific files
49 | # -------------------------
50 | .vscode/
51 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/index.ts:
--------------------------------------------------------------------------------
1 | import { Dialog as DialogPrimitive } from 'bits-ui';
2 |
3 | import Close from './dialog-close.svelte';
4 | import Content from './dialog-content.svelte';
5 | import Description from './dialog-description.svelte';
6 | import Footer from './dialog-footer.svelte';
7 | import Header from './dialog-header.svelte';
8 | import Overlay from './dialog-overlay.svelte';
9 | import Title from './dialog-title.svelte';
10 | import Trigger from './dialog-trigger.svelte';
11 |
12 | const Root = DialogPrimitive.Root;
13 | const Portal = DialogPrimitive.Portal;
14 |
15 | export {
16 | Root,
17 | Title,
18 | Portal,
19 | Footer,
20 | Header,
21 | Trigger,
22 | Overlay,
23 | Content,
24 | Description,
25 | Close,
26 | //
27 | Root as Dialog,
28 | Title as DialogTitle,
29 | Portal as DialogPortal,
30 | Footer as DialogFooter,
31 | Header as DialogHeader,
32 | Trigger as DialogTrigger,
33 | Overlay as DialogOverlay,
34 | Content as DialogContent,
35 | Description as DialogDescription,
36 | Close as DialogClose
37 | };
38 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-field.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | {#snippet children({ constraints, errors, tainted, value })}
22 |
23 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
24 |
25 | {/snippet}
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/ui/form/form-element-field.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | {#snippet children({ constraints, errors, tainted, value })}
22 |
23 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
24 |
25 | {/snippet}
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/ui/textarea/textarea.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tabs/tabs-trigger.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/lib/utils/mail/templates/welcome.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 | Welcome to SvelteKit Omakase
24 |
25 |
26 | Hey, {userFirstName}!
27 | We're thrilled to have you onboard. ⭐
28 |
29 |
30 |
31 |
32 | Need help? we are just an email away.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
30 | {@render children?.()}
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/index.ts:
--------------------------------------------------------------------------------
1 | import { Select as SelectPrimitive } from 'bits-ui';
2 |
3 | import Content from './select-content.svelte';
4 | import GroupHeading from './select-group-heading.svelte';
5 | import Group from './select-group.svelte';
6 | import Item from './select-item.svelte';
7 | import Label from './select-label.svelte';
8 | import ScrollDownButton from './select-scroll-down-button.svelte';
9 | import ScrollUpButton from './select-scroll-up-button.svelte';
10 | import Separator from './select-separator.svelte';
11 | import Trigger from './select-trigger.svelte';
12 |
13 | const Root = SelectPrimitive.Root;
14 |
15 | export {
16 | Root,
17 | Group,
18 | Label,
19 | Item,
20 | Content,
21 | Trigger,
22 | Separator,
23 | ScrollDownButton,
24 | ScrollUpButton,
25 | GroupHeading,
26 | //
27 | Root as Select,
28 | Group as SelectGroup,
29 | Label as SelectLabel,
30 | Item as SelectItem,
31 | Content as SelectContent,
32 | Trigger as SelectTrigger,
33 | Separator as SelectSeparator,
34 | ScrollDownButton as SelectScrollDownButton,
35 | ScrollUpButton as SelectScrollUpButton,
36 | GroupHeading as SelectGroupHeading
37 | };
38 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
30 | {#if showIcon}
31 |
32 | {/if}
33 |
38 | {@render children?.()}
39 |
40 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # -------------------------
2 | # OS-specific files
3 | # -------------------------
4 | .DS_Store
5 | Thumbs.db
6 |
7 | # -------------------------
8 | # Node.js dependencies
9 | # -------------------------
10 | node_modules/
11 |
12 | # -------------------------
13 | # Build output
14 | # -------------------------
15 | /build
16 | /.svelte-kit
17 | /package
18 | /dist
19 |
20 | # -------------------------
21 | # Environment & secrets
22 | # -------------------------
23 | .env
24 | .env.*
25 | !.env.example
26 |
27 | # -------------------------
28 | # Databases & local data
29 | # -------------------------
30 | dev.db
31 | *.db
32 | /src/lib/db/migrations
33 |
34 | # -------------------------
35 | # Test results
36 | # -------------------------
37 | /test-results
38 |
39 | # -------------------------
40 | # Editor-specific files
41 | # -------------------------
42 | .vscode/
43 |
44 | # -------------------------
45 | # Lockfiles
46 | # -------------------------
47 | package-lock.json
48 | pnpm-lock.yaml
49 | yarn.lock
50 | bun.lock
51 | bun.lockb
52 |
53 | # -------------------------
54 | # Miscellaneous
55 | # -------------------------
56 | .gitignore
57 | .prettierrc
58 | .prettierignore
59 | .husky/
60 | .npmrc
61 | /static/
--------------------------------------------------------------------------------
/src/lib/components/ui/popover/popover-content.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/app-sidebar.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/lib/components/ui/toggle-group/toggle-group-item.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
40 |
--------------------------------------------------------------------------------
/src/routes/(app)/settings/+layout.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
Settings
26 |
Manage your account settings and preferences.
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 | {@render children?.()}
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
28 | {@render children?.()}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group-label.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 | {#if child}
31 | {@render child({ props: mergedProps })}
32 | {:else}
33 |
34 | {@render children?.()}
35 |
36 | {/if}
37 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/index.ts:
--------------------------------------------------------------------------------
1 | import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
2 |
3 | import Action from './alert-dialog-action.svelte';
4 | import Cancel from './alert-dialog-cancel.svelte';
5 | import Content from './alert-dialog-content.svelte';
6 | import Description from './alert-dialog-description.svelte';
7 | import Footer from './alert-dialog-footer.svelte';
8 | import Header from './alert-dialog-header.svelte';
9 | import Overlay from './alert-dialog-overlay.svelte';
10 | import Title from './alert-dialog-title.svelte';
11 | import Trigger from './alert-dialog-trigger.svelte';
12 |
13 | const Root = AlertDialogPrimitive.Root;
14 | const Portal = AlertDialogPrimitive.Portal;
15 |
16 | export {
17 | Root,
18 | Title,
19 | Action,
20 | Cancel,
21 | Portal,
22 | Footer,
23 | Header,
24 | Trigger,
25 | Overlay,
26 | Content,
27 | Description,
28 | //
29 | Root as AlertDialog,
30 | Title as AlertDialogTitle,
31 | Action as AlertDialogAction,
32 | Cancel as AlertDialogCancel,
33 | Portal as AlertDialogPortal,
34 | Footer as AlertDialogFooter,
35 | Header as AlertDialogHeader,
36 | Trigger as AlertDialogTrigger,
37 | Overlay as AlertDialogOverlay,
38 | Content as AlertDialogContent,
39 | Description as AlertDialogDescription
40 | };
41 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
31 |
32 |
--------------------------------------------------------------------------------
/src/routes/api/upload/+server.ts:
--------------------------------------------------------------------------------
1 | import { PUBLIC_R2_BUCKET_NAME } from '$env/static/public';
2 |
3 | import type { RequestHandler } from '@sveltejs/kit';
4 |
5 | import crypto from 'crypto';
6 | import { PutObjectCommand } from '@aws-sdk/client-s3';
7 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
8 | import { error, json } from '@sveltejs/kit';
9 |
10 | import { auth } from '$lib/server/auth';
11 | import { s3 } from '$lib/server/storage';
12 |
13 | export const POST: RequestHandler = async ({ request }) => {
14 | const session = await auth.api.getSession(request);
15 |
16 | if (!session?.user) {
17 | error(401, 'Unauthorized');
18 | }
19 |
20 | try {
21 | const { fileType, destinationDirectory } = await request.json();
22 |
23 | const fileName = crypto.randomBytes(16).toString('hex');
24 |
25 | const file = {
26 | Bucket: PUBLIC_R2_BUCKET_NAME,
27 | Key: `${destinationDirectory}/${fileName}`,
28 | ContentType: fileType
29 | };
30 |
31 | const command = new PutObjectCommand(file);
32 | const url = await getSignedUrl(s3, command, { expiresIn: 60000 });
33 |
34 | return json({
35 | presignedUrl: url,
36 | fileName
37 | });
38 | } catch (err) {
39 | console.log(err);
40 | }
41 |
42 | error(500, 'Something went wrong');
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
28 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
26 | {#snippet children({ checked })}
27 |
28 | {#if checked}
29 |
30 | {/if}
31 |
32 | {@render childrenProp?.({ checked })}
33 | {/snippet}
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/utils/mail/templates/reset-password.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 🔒 Reset Your Password
21 |
22 |
23 | Hey, {userFirstName}!
24 | It looks like you requested a password reset. No worries, we've got you covered!
25 | Click the link below to reset your password:
26 |
27 | Reset Your Password
28 |
29 |
30 |
31 |
32 |
33 | If you didn't request this, please ignore this email or
34 | contact us.
35 |
36 | Stay secure 🛟
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/ui/switch/switch.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
26 |
32 |
33 |
--------------------------------------------------------------------------------
/src/lib/components/ui/radio-group/radio-group-item.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
25 | {#snippet children({ checked })}
26 |
27 | {#if checked}
28 |
29 | {/if}
30 |
31 | {/snippet}
32 |
33 |
--------------------------------------------------------------------------------
/src/lib/components/ui/accordion/accordion-trigger.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | svg]:rotate-180',
26 | className
27 | )}
28 | {...restProps}
29 | >
30 | {@render children?.()}
31 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/lib/components/settings-navbar.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
47 |
--------------------------------------------------------------------------------
/src/lib/components/ui/toggle-group/toggle-group.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
34 |
35 |
39 |
48 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-group-action.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 | {#if child}
33 | {@render child({ props: mergedProps })}
34 | {:else}
35 |
38 | {/if}
39 |
--------------------------------------------------------------------------------
/src/lib/components/ui/alert/alert.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
38 |
39 |
40 | {@render children?.()}
41 |
42 |
--------------------------------------------------------------------------------
/src/lib/hooks/use-theme.ts:
--------------------------------------------------------------------------------
1 | import { resetMode, setMode, userPrefersMode } from 'mode-watcher';
2 |
3 | /**
4 | * Cycles through theme modes: system → light → dark → system
5 | */
6 | export function cycleThemeMode(): void {
7 | const currentUserMode = userPrefersMode.current;
8 | if (currentUserMode === 'system') {
9 | setMode('light');
10 | } else if (currentUserMode === 'light') {
11 | setMode('dark');
12 | } else {
13 | resetMode();
14 | }
15 | }
16 |
17 | /**
18 | * Sets up a keyboard listener for theme cycling.
19 | * Listens for 't' key and cycles through themes.
20 | * Ignores key presses when typing in input fields.
21 | *
22 | * @returns Cleanup function to remove the event listener
23 | */
24 | export function setupThemeCyclingKeyListener(): () => void {
25 | const keyListener = (e: KeyboardEvent) => {
26 | // Ignore key presses when typing in input fields
27 | if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
28 | return;
29 | }
30 |
31 | if (e.key === 't') {
32 | cycleThemeMode();
33 | }
34 | };
35 |
36 | window.addEventListener('keydown', keyListener);
37 |
38 | // Return cleanup function
39 | return () => {
40 | window.removeEventListener('keydown', keyListener);
41 | };
42 | }
43 |
44 | /**
45 | * Custom hook for theme cycling with keyboard support.
46 | */
47 | export function useTheme() {
48 | return {
49 | cycleMode: cycleThemeMode,
50 | setupKeyListener: setupThemeCyclingKeyListener
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-item.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
29 | {#snippet children({ selected, highlighted })}
30 |
31 | {#if selected}
32 |
33 | {/if}
34 |
35 | {#if childrenProp}
36 | {@render childrenProp({ selected, highlighted })}
37 | {:else}
38 | {label || value}
39 | {/if}
40 | {/snippet}
41 |
42 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {@render children?.()}
62 |
63 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-rail.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
40 |
--------------------------------------------------------------------------------
/src/lib/validations/files.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const MAX_AVATAR_SIZE = 2000000; // 2MB
4 | const MAX_IMAGE_SIZE = 4000000; // 4MB
5 | const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
6 |
7 | const imageFileSchema = z
8 | .instanceof(File)
9 | .refine((file) => file.size <= MAX_IMAGE_SIZE, {
10 | error: 'Image size must be less than 4MB.'
11 | })
12 | .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
13 | error: 'Only .jpg, .jpeg, .png and .webp formats are supported.'
14 | });
15 |
16 | export const avatarFileSchema = z
17 | .instanceof(File)
18 | .refine((file) => file.size <= MAX_AVATAR_SIZE, {
19 | error: 'Avatar size must be less than 2MB.'
20 | })
21 | .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
22 | error: 'Only .jpg, .jpeg, .png and .webp formats are supported.'
23 | });
24 |
25 | export function validateImageFile(
26 | imageFile: File,
27 | type: string
28 | ): {
29 | valid: boolean;
30 | errors: string[];
31 | } {
32 | const schema = type === 'avatar' ? avatarFileSchema : imageFileSchema;
33 | const result = schema.safeParse(imageFile);
34 |
35 | return {
36 | valid: result.success,
37 | errors: result.success ? [] : result.error.issues.map((e) => e.message)
38 | };
39 | }
40 |
41 | // Avatar file validations
42 | export function validateAvatarFile(avatarFile: File): {
43 | valid: boolean;
44 | errors: string[];
45 | } {
46 | const result = avatarFileSchema.safeParse(avatarFile);
47 |
48 | return {
49 | valid: result.success,
50 | errors: result.success ? [] : result.error.issues.map((e) => e.message)
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/src/lib/components/app-head.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 | {displayTitle}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-trigger.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
30 | {@render children?.()}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/lib/components/ui/checkbox/checkbox.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
30 | {#snippet children({ checked, indeterminate })}
31 |
32 | {#if checked}
33 |
34 | {:else if indeterminate}
35 |
36 | {/if}
37 |
38 | {/snippet}
39 |
40 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-provider.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
40 |
49 |
50 |
--------------------------------------------------------------------------------
/src/lib/components/app-sidebar-content.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | Platform
23 |
24 | {#each items as item (item.title)}
25 | {@const isActive = page.url.pathname === item.url}
26 |
27 |
28 | {#snippet child({ props })}
29 |
30 | {#if item.icon}
31 | {@const Icon = item.icon}
32 |
33 | {/if}
34 | {item.title}
35 |
36 | {/snippet}
37 |
38 | {/each}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {#snippet child({ props })}
48 |
49 |
50 | GitHub
51 |
52 | {/snippet}
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
34 | {#snippet children({ checked, indeterminate })}
35 |
36 | {#if indeterminate}
37 |
38 | {:else}
39 |
40 | {/if}
41 |
42 | {@render childrenProp?.()}
43 | {/snippet}
44 |
45 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url';
2 | import { includeIgnoreFile } from '@eslint/compat';
3 | import js from '@eslint/js';
4 | import prettier from 'eslint-config-prettier';
5 | import svelte from 'eslint-plugin-svelte';
6 | import unicorn from 'eslint-plugin-unicorn';
7 | import { defineConfig } from 'eslint/config';
8 | import globals from 'globals';
9 | import ts from 'typescript-eslint';
10 |
11 | import svelteConfig from './svelte.config.js';
12 |
13 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
14 |
15 | export default defineConfig([
16 | includeIgnoreFile(gitignorePath),
17 | js.configs.recommended,
18 | ...ts.configs.recommended,
19 | ...svelte.configs.recommended,
20 | prettier,
21 | ...svelte.configs.prettier,
22 | {
23 | plugins: {
24 | unicorn
25 | },
26 | languageOptions: {
27 | globals: { ...globals.browser, ...globals.node }
28 | },
29 | rules: {
30 | 'no-undef': 'off',
31 | 'unicorn/filename-case': [
32 | 'error',
33 | {
34 | case: 'kebabCase',
35 | ignore: ['^\\.[a-z]+rc\\.(js|ts|json)$', '^[A-Z]+\\.(md|txt)$']
36 | }
37 | ]
38 | }
39 | },
40 | {
41 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
42 | languageOptions: {
43 | parserOptions: {
44 | projectService: true,
45 | extraFileExtensions: ['.svelte'],
46 | parser: ts.parser,
47 | svelteConfig
48 | }
49 | },
50 | rules: {
51 | 'svelte/no-navigation-without-resolve': [
52 | 'error',
53 | {
54 | ignoreLinks: true
55 | }
56 | ]
57 | }
58 | }
59 | // Disable rules for shadcn-svelte UI components (OPTIONAL)
60 | // {
61 | // ignores: ['src/lib/components/ui/**']
62 | // }
63 | ]);
64 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dropdown-menu/index.ts:
--------------------------------------------------------------------------------
1 | import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
2 |
3 | import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
4 | import Content from './dropdown-menu-content.svelte';
5 | import GroupHeading from './dropdown-menu-group-heading.svelte';
6 | import Group from './dropdown-menu-group.svelte';
7 | import Item from './dropdown-menu-item.svelte';
8 | import Label from './dropdown-menu-label.svelte';
9 | import RadioGroup from './dropdown-menu-radio-group.svelte';
10 | import RadioItem from './dropdown-menu-radio-item.svelte';
11 | import Separator from './dropdown-menu-separator.svelte';
12 | import Shortcut from './dropdown-menu-shortcut.svelte';
13 | import SubContent from './dropdown-menu-sub-content.svelte';
14 | import SubTrigger from './dropdown-menu-sub-trigger.svelte';
15 | import Trigger from './dropdown-menu-trigger.svelte';
16 |
17 | const Sub = DropdownMenuPrimitive.Sub;
18 | const Root = DropdownMenuPrimitive.Root;
19 |
20 | export {
21 | CheckboxItem,
22 | Content,
23 | Root as DropdownMenu,
24 | CheckboxItem as DropdownMenuCheckboxItem,
25 | Content as DropdownMenuContent,
26 | Group as DropdownMenuGroup,
27 | Item as DropdownMenuItem,
28 | Label as DropdownMenuLabel,
29 | RadioGroup as DropdownMenuRadioGroup,
30 | RadioItem as DropdownMenuRadioItem,
31 | Separator as DropdownMenuSeparator,
32 | Shortcut as DropdownMenuShortcut,
33 | Sub as DropdownMenuSub,
34 | SubContent as DropdownMenuSubContent,
35 | SubTrigger as DropdownMenuSubTrigger,
36 | Trigger as DropdownMenuTrigger,
37 | GroupHeading as DropdownMenuGroupHeading,
38 | Group,
39 | GroupHeading,
40 | Item,
41 | Label,
42 | RadioGroup,
43 | RadioItem,
44 | Root,
45 | Separator,
46 | Shortcut,
47 | Sub,
48 | SubContent,
49 | SubTrigger,
50 | Trigger
51 | };
52 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte:
--------------------------------------------------------------------------------
1 |
38 |
39 | {#if child}
40 | {@render child({ props: mergedProps })}
41 | {:else}
42 |
43 | {@render children?.()}
44 |
45 | {/if}
46 |
--------------------------------------------------------------------------------
/src/routes/(auth)/password/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { Action, Actions } from './$types';
2 |
3 | import { redirect } from 'sveltekit-flash-message/server';
4 | import { zod4 } from 'sveltekit-superforms/adapters';
5 | import { superValidate } from 'sveltekit-superforms/server';
6 |
7 | import { auth, redirectIfLoggedIn } from '$lib/server/auth';
8 | import { isRateLimited, setFormError, setFormFail } from '$lib/utils/helpers/forms';
9 | import * as m from '$lib/utils/messages.json';
10 | import { requestPasswordResetSchema } from '$lib/validations/auth';
11 |
12 | export async function load() {
13 | redirectIfLoggedIn();
14 |
15 | const form = await superValidate(zod4(requestPasswordResetSchema));
16 |
17 | return {
18 | metadata: {
19 | title: 'Request Password Reset'
20 | },
21 | form
22 | };
23 | }
24 |
25 | const requestPasswordReset: Action = async (event) => {
26 | const form = await superValidate(event.request, zod4(requestPasswordResetSchema));
27 |
28 | await isRateLimited(form, event, { field: 'email' });
29 |
30 | if (!form.valid) {
31 | return setFormFail(form);
32 | }
33 |
34 | const { email } = form.data;
35 |
36 | try {
37 | await auth.api.requestPasswordReset({
38 | body: {
39 | email,
40 | redirectTo: `/password/reset?email=${email}`
41 | }
42 | });
43 | } catch (error) {
44 | console.log(error);
45 | return setFormError(form, m.general.error, {
46 | status: 500
47 | });
48 | }
49 |
50 | // we send a success message even if the user doesn't exist to prevent email enumeration
51 | redirect(
52 | '/',
53 | {
54 | type: 'success',
55 | message: m.auth.requestResetPassword.success
56 | },
57 | event
58 | );
59 | };
60 |
61 | export const actions = {
62 | default: requestPasswordReset
63 | } satisfies Actions;
64 |
--------------------------------------------------------------------------------
/src/lib/db/clear.ts:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 | import { sql } from 'drizzle-orm';
3 |
4 | import db from '$lib/server/database';
5 |
6 | async function clearDb() {
7 | config();
8 |
9 | if (process.env.NODE_ENV === 'production') {
10 | console.error('Clear script should not be run in production!');
11 | process.exit(1);
12 | }
13 |
14 | const tableSchema = db._.schema;
15 | if (!tableSchema) {
16 | throw new Error('No table schema found');
17 | }
18 |
19 | console.log('🗑️ Emptying the entire database');
20 |
21 | const dropForeignKeys = sql.raw('PRAGMA foreign_keys = OFF;');
22 | const enableForeignKeys = sql.raw('PRAGMA foreign_keys = ON;');
23 |
24 | const queries = Object.values(tableSchema).map((table) => {
25 | console.log(`🧨 Preparing DELETE query for table: ${table.dbName}`);
26 | return {
27 | query: sql.raw(`DELETE FROM ${table.dbName}; DELETE FROM sqlite_sequence WHERE name='${table.dbName}';`),
28 | queryString: `DELETE FROM ${table.dbName}; DELETE FROM sqlite_sequence WHERE name='${table.dbName}';`
29 | };
30 | });
31 |
32 | try {
33 | console.log('🔒 Disabling foreign keys...');
34 | await db.run(dropForeignKeys);
35 |
36 | await db.transaction(async (tx) => {
37 | console.log('📨 Sending queries...');
38 | for (const { query, queryString } of queries) {
39 | console.log(`💽 Executing query: ${queryString}`);
40 | await tx.run(query);
41 | }
42 | });
43 |
44 | console.log('🔓 Enabling foreign keys...');
45 | await db.run(enableForeignKeys);
46 |
47 | console.log('✅ Database emptied');
48 | } catch (error) {
49 | console.error('❌ Error occurred while clearing the database:', error);
50 | process.exit(1);
51 | }
52 | }
53 |
54 | clearDb().catch((e) => {
55 | console.error(e);
56 | process.exit(1);
57 | });
58 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/sidebar-menu-action.svelte:
--------------------------------------------------------------------------------
1 |
38 |
39 | {#if child}
40 | {@render child({ props: mergedProps })}
41 | {:else}
42 |
45 | {/if}
46 |
--------------------------------------------------------------------------------
/src/lib/components/ui/tooltip/tooltip-content.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
31 | {@render children?.()}
32 |
33 | {#snippet child({ props })}
34 |
45 | {/snippet}
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/routes/(auth)/password/+page.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 | Request Password Reset
25 | Enter your email below to receive a password reset link
26 |
27 |
28 |
30 | {#snippet children({ constraints })}
31 |
32 | {#snippet children({ props })}
33 | Email
34 |
42 |
43 | {/snippet}
44 |
45 | {/snippet}
46 |
47 |
48 |
49 | {#if $delayed}
50 |
51 | {/if}
52 | Send
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/lib/components/ui/dialog/dialog-content.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
37 | {@render children?.()}
38 | {#if showCloseButton}
39 |
42 |
43 | Close
44 |
45 | {/if}
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/lib/components/ui/select/select-content.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
34 |
35 |
38 | {@render children?.()}
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/lib/db/migrations/0000_tricky_angel.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `account` (
2 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3 | `account_id` text NOT NULL,
4 | `provider_id` text NOT NULL,
5 | `user_id` text NOT NULL,
6 | `access_token` text,
7 | `refresh_token` text,
8 | `id_token` text,
9 | `access_token_expires_at` integer,
10 | `refresh_token_expires_at` integer,
11 | `scope` text,
12 | `password` text,
13 | `created_at` integer NOT NULL,
14 | `updated_at` integer NOT NULL,
15 | FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
16 | );
17 | --> statement-breakpoint
18 | CREATE TABLE `session` (
19 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
20 | `token` text NOT NULL,
21 | `created_at` integer NOT NULL,
22 | `updated_at` integer NOT NULL,
23 | `expires_at` integer NOT NULL,
24 | `ip_address` text,
25 | `user_agent` text,
26 | `user_id` text NOT NULL,
27 | FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
28 | );
29 | --> statement-breakpoint
30 | CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
31 | CREATE TABLE `user` (
32 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
33 | `public_id` text,
34 | `email` text NOT NULL,
35 | `email_verified` integer DEFAULT false NOT NULL,
36 | `first_name` text NOT NULL,
37 | `last_name` text NOT NULL,
38 | `name` text NOT NULL,
39 | `avatar` text,
40 | `image` text,
41 | `created_at` integer NOT NULL,
42 | `updated_at` integer NOT NULL
43 | );
44 | --> statement-breakpoint
45 | CREATE UNIQUE INDEX `user_publicId_unique` ON `user` (`public_id`);--> statement-breakpoint
46 | CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
47 | CREATE UNIQUE INDEX `public_id_index` ON `user` (`public_id`);--> statement-breakpoint
48 | CREATE TABLE `verification` (
49 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
50 | `identifier` text NOT NULL,
51 | `value` text NOT NULL,
52 | `expires_at` integer NOT NULL,
53 | `created_at` integer NOT NULL,
54 | `updated_at` integer NOT NULL
55 | );
56 |
--------------------------------------------------------------------------------
/src/lib/components/ui/badge/badge.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
42 |
43 |
51 | {@render children?.()}
52 |
53 |
--------------------------------------------------------------------------------
/src/lib/components/ui/slider/slider.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
21 |
32 | {#snippet children({ thumbs })}
33 |
40 |
44 |
45 | {#each thumbs as thumb (thumb)}
46 |
51 | {/each}
52 | {/snippet}
53 |
54 |
--------------------------------------------------------------------------------
/src/lib/components/ui/toggle/toggle.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
47 |
48 |
55 |
--------------------------------------------------------------------------------
/src/routes/(auth)/login/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { Action, Actions } from './$types';
2 |
3 | import { APIError as BetterAuthAPIError } from 'better-auth/api';
4 | import { redirect } from 'sveltekit-flash-message/server';
5 | import { zod4 } from 'sveltekit-superforms/adapters';
6 | import { superValidate } from 'sveltekit-superforms/server';
7 |
8 | import { auth, redirectIfLoggedIn } from '$lib/server/auth';
9 | import { isRateLimited, setFormError, setFormFail } from '$lib/utils/helpers/forms';
10 | import * as m from '$lib/utils/messages.json';
11 | import { loginSchema } from '$lib/validations/auth';
12 |
13 | export async function load() {
14 | redirectIfLoggedIn();
15 |
16 | const form = await superValidate(zod4(loginSchema));
17 |
18 | return {
19 | metadata: {
20 | title: 'Login'
21 | },
22 | form
23 | };
24 | }
25 |
26 | const login: Action = async (event) => {
27 | const form = await superValidate(event.request, zod4(loginSchema));
28 |
29 | await isRateLimited(form, event, { field: 'email', removeSensitiveData: ['password'] });
30 |
31 | if (!form.valid) {
32 | return setFormFail(form, { removeSensitiveData: ['password'] });
33 | }
34 |
35 | const { email, password } = form.data;
36 |
37 | try {
38 | await auth.api.signInEmail({
39 | body: {
40 | email,
41 | password
42 | },
43 | headers: event.request.headers
44 | });
45 | } catch (error) {
46 | if (error instanceof BetterAuthAPIError) {
47 | console.log(error);
48 | if (error.body?.code === 'INVALID_EMAIL_OR_PASSWORD') {
49 | return setFormError(
50 | form,
51 | m.auth.login.error,
52 | {
53 | field: 'email',
54 | removeSensitiveData: ['password', 'passwordConfirmation']
55 | },
56 | event
57 | );
58 | }
59 | }
60 | return setFormFail(form, {
61 | removeSensitiveData: ['password']
62 | });
63 | }
64 |
65 | redirect(
66 | '/',
67 | {
68 | status: 303,
69 | type: 'success',
70 | message: m.auth.login.success
71 | },
72 | event
73 | );
74 | };
75 |
76 | export const actions = {
77 | default: login
78 | } satisfies Actions;
79 |
--------------------------------------------------------------------------------
/src/lib/components/ui/input/input.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 | {#if type === 'file'}
24 |
38 | {:else}
39 |
52 | {/if}
53 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | env:
10 | BUN_VERSION: 'latest'
11 |
12 | jobs:
13 | quality:
14 | name: Code Quality
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 15
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 |
22 | - name: Setup Bun
23 | uses: oven-sh/setup-bun@v2
24 | with:
25 | bun-version: ${{ env.BUN_VERSION }}
26 |
27 | - name: Cache Bun dependencies
28 | uses: actions/cache@v4
29 | with:
30 | path: |
31 | ~/.bun/install/cache
32 | ./.bun
33 | key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
34 | restore-keys: ${{ runner.os }}-bun-
35 |
36 | - name: Install dependencies
37 | run: |
38 | if [ -f bun.lock ]; then
39 | bun install --frozen-lockfile
40 | else
41 | echo "⚠️ No bun.lock found — installing without frozen lockfile"
42 | bun install
43 | fi
44 |
45 | - name: Run Prettier
46 | run: bun run prettier --check .
47 |
48 | - name: Run ESLint
49 | run: bun run eslint .
50 |
51 | tests:
52 | name: Tests
53 | runs-on: ubuntu-latest
54 | timeout-minutes: 15
55 |
56 | steps:
57 | - name: Checkout
58 | uses: actions/checkout@v4
59 |
60 | - name: Setup Bun
61 | uses: oven-sh/setup-bun@v2
62 | with:
63 | bun-version: ${{ env.BUN_VERSION }}
64 |
65 | - name: Cache Bun dependencies
66 | uses: actions/cache@v4
67 | with:
68 | path: |
69 | ~/.bun/install/cache
70 | ./.bun
71 | key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
72 | restore-keys: ${{ runner.os }}-bun-
73 |
74 | - name: Install dependencies
75 | run: |
76 | if [ -f bun.lock ]; then
77 | bun install --frozen-lockfile
78 | else
79 | echo "⚠️ No bun.lock found — installing without frozen lockfile"
80 | bun install
81 | fi
82 |
83 | - name: Run unit tests
84 | run: bun run test:unit
85 |
--------------------------------------------------------------------------------
/src/lib/utils/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "general": {
3 | "error": "Something went wrong. Please try again."
4 | },
5 | "pages": {
6 | "notFound": {
7 | "title": "404 - Page Not Found",
8 | "message": "Oops! We can't seem to find the page you're looking for."
9 | }
10 | },
11 | "auth": {
12 | "unauthorized": "Access denied. You do not have permission to view this page.",
13 | "login": {
14 | "success": "You're now logged in.",
15 | "registeredWithDifferentMethod": "This account was created with a different method. Please log in with the same method you used to register.",
16 | "error": "Login failed. Please check your email and password and try again."
17 | },
18 | "logout": {
19 | "success": "You've been logged out successfully."
20 | },
21 | "register": {
22 | "success": "Awesome! You're all set to go.",
23 | "emailIsTaken": "This email is already in use. Try another?",
24 | "passwordsMismatch": "Oops! The passwords don't match. Try again?"
25 | },
26 | "requestResetPassword": {
27 | "success": "Check your inbox! We've sent you a link to reset your password."
28 | },
29 | "resetPassword": {
30 | "success": "All set! Your password has been reset successfully. You can now log in with your new password.",
31 | "invalidToken": "Your password reset link is invalid or has expired. Please request a new one.",
32 | "passwordsMismatch": "The passwords don't match. Please try again."
33 | }
34 | },
35 | "settings": {
36 | "userProfile": {
37 | "edit": {
38 | "success": "Your profile has been updated successfully.",
39 | "noChanges": "Looks like there were no changes to save."
40 | },
41 | "delete": {
42 | "success": "Your user account has been permanently deleted.",
43 | "destructiveOperation": "Are you sure you want to delete your user account? This cannot be undone."
44 | }
45 | },
46 | "password": {
47 | "edit": {
48 | "success": {
49 | "set": "Password set successfully! You can now log in using your email and password in addition to your existing login method.",
50 | "update": "Your password has been updated successfully."
51 | }
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/components/theme-switch.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 | {#if mode.current}
32 |
33 |
34 |
35 | {#snippet child({ props })}
36 |
58 | {/snippet}
59 |
60 |
61 |
62 | switch to {nextMode} mode
63 | T
64 |
65 |
66 |
67 |
68 | {/if}
69 |
--------------------------------------------------------------------------------
/src/routes/(auth)/password/reset/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { Action, Actions } from './$types';
2 |
3 | import { redirect } from 'sveltekit-flash-message/server';
4 | import { zod4 } from 'sveltekit-superforms/adapters';
5 | import { superValidate } from 'sveltekit-superforms/server';
6 |
7 | import { auth, redirectIfLoggedIn } from '$lib/server/auth';
8 | import { setFormError, setFormFail } from '$lib/utils/helpers/forms';
9 | import * as m from '$lib/utils/messages.json';
10 | import { passwordResetSchema } from '$lib/validations/auth';
11 |
12 | export async function load(event) {
13 | redirectIfLoggedIn();
14 |
15 | const token = event.url.searchParams.get('token');
16 | const email = event.url.searchParams.get('email');
17 | const error = event.url.searchParams.get('error');
18 |
19 | if (error || !token || !email) {
20 | if (error) console.log(`Password Reset Error: ${error}`);
21 |
22 | redirect(
23 | '/password',
24 | {
25 | type: 'error',
26 | message: m.auth.resetPassword.invalidToken
27 | },
28 | event
29 | );
30 | } else {
31 | const form = await superValidate({ email, token }, zod4(passwordResetSchema), {
32 | errors: false
33 | });
34 |
35 | return {
36 | metadata: {
37 | title: 'Password Reset'
38 | },
39 | form
40 | };
41 | }
42 | }
43 |
44 | const resetPassword: Action = async (event) => {
45 | const form = await superValidate(event.request, zod4(passwordResetSchema));
46 |
47 | if (!form.valid) {
48 | return setFormFail(form);
49 | }
50 |
51 | const { token, password, passwordConfirmation } = form.data;
52 |
53 | if (password !== passwordConfirmation) {
54 | return setFormError(
55 | form,
56 | m.auth.register.passwordsMismatch,
57 | {
58 | field: 'passwordConfirmation',
59 | removeSensitiveData: ['password', 'passwordConfirmation']
60 | },
61 | event
62 | );
63 | }
64 |
65 | try {
66 | await auth.api.resetPassword({ body: { newPassword: password, token } });
67 | } catch (error) {
68 | console.log(error);
69 | return setFormError(form, m.general.error, {
70 | status: 500
71 | });
72 | }
73 |
74 | redirect(
75 | '/login',
76 | {
77 | type: 'success',
78 | message: m.auth.resetPassword.success
79 | },
80 | event
81 | );
82 | };
83 |
84 | export const actions = {
85 | default: resetPassword
86 | } satisfies Actions;
87 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sidebar/index.ts:
--------------------------------------------------------------------------------
1 | import { useSidebar } from './context.svelte.js';
2 | import Content from './sidebar-content.svelte';
3 | import Footer from './sidebar-footer.svelte';
4 | import GroupAction from './sidebar-group-action.svelte';
5 | import GroupContent from './sidebar-group-content.svelte';
6 | import GroupLabel from './sidebar-group-label.svelte';
7 | import Group from './sidebar-group.svelte';
8 | import Header from './sidebar-header.svelte';
9 | import Input from './sidebar-input.svelte';
10 | import Inset from './sidebar-inset.svelte';
11 | import MenuAction from './sidebar-menu-action.svelte';
12 | import MenuBadge from './sidebar-menu-badge.svelte';
13 | import MenuButton from './sidebar-menu-button.svelte';
14 | import MenuItem from './sidebar-menu-item.svelte';
15 | import MenuSkeleton from './sidebar-menu-skeleton.svelte';
16 | import MenuSubButton from './sidebar-menu-sub-button.svelte';
17 | import MenuSubItem from './sidebar-menu-sub-item.svelte';
18 | import MenuSub from './sidebar-menu-sub.svelte';
19 | import Menu from './sidebar-menu.svelte';
20 | import Provider from './sidebar-provider.svelte';
21 | import Rail from './sidebar-rail.svelte';
22 | import Separator from './sidebar-separator.svelte';
23 | import Trigger from './sidebar-trigger.svelte';
24 | import Root from './sidebar.svelte';
25 |
26 | export {
27 | Content,
28 | Footer,
29 | Group,
30 | GroupAction,
31 | GroupContent,
32 | GroupLabel,
33 | Header,
34 | Input,
35 | Inset,
36 | Menu,
37 | MenuAction,
38 | MenuBadge,
39 | MenuButton,
40 | MenuItem,
41 | MenuSkeleton,
42 | MenuSub,
43 | MenuSubButton,
44 | MenuSubItem,
45 | Provider,
46 | Rail,
47 | Root,
48 | Separator,
49 | //
50 | Root as Sidebar,
51 | Content as SidebarContent,
52 | Footer as SidebarFooter,
53 | Group as SidebarGroup,
54 | GroupAction as SidebarGroupAction,
55 | GroupContent as SidebarGroupContent,
56 | GroupLabel as SidebarGroupLabel,
57 | Header as SidebarHeader,
58 | Input as SidebarInput,
59 | Inset as SidebarInset,
60 | Menu as SidebarMenu,
61 | MenuAction as SidebarMenuAction,
62 | MenuBadge as SidebarMenuBadge,
63 | MenuButton as SidebarMenuButton,
64 | MenuItem as SidebarMenuItem,
65 | MenuSkeleton as SidebarMenuSkeleton,
66 | MenuSub as SidebarMenuSub,
67 | MenuSubButton as SidebarMenuSubButton,
68 | MenuSubItem as SidebarMenuSubItem,
69 | Provider as SidebarProvider,
70 | Rail as SidebarRail,
71 | Separator as SidebarSeparator,
72 | Trigger as SidebarTrigger,
73 | Trigger,
74 | useSidebar
75 | };
76 |
--------------------------------------------------------------------------------
/src/routes/(auth)/password/reset/+page.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 | Reset Your Password
25 | Enter a new password for {$formData.email}
26 |
27 |
28 |
32 | {#snippet children({ constraints })}
33 |
34 | {#snippet children({ props })}
35 | Password
36 |
43 |
44 | {/snippet}
45 |
46 | {/snippet}
47 |
48 |
49 |
50 | {#snippet children({ constraints })}
51 |
52 | {#snippet children({ props })}
53 | Password Confirmation
54 |
61 |
62 | {/snippet}
63 |
64 | {/snippet}
65 |
66 |
67 |
68 | {#if $delayed}
69 |
70 | {/if}
71 | Reset
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/lib/components/ui/sheet/sheet-content.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 |
50 |
51 |
52 |
53 |
59 | {@render children?.()}
60 |
63 |
64 | Close
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------