├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── components.json ├── drizzle.config.ts ├── eslint.config.js ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── src ├── app.d.ts ├── app.html ├── hooks.server.ts ├── lib │ ├── assets │ │ ├── avatar.png │ │ ├── logo.png │ │ └── meta_image.png │ ├── components │ │ ├── PlaceholderPattern.svelte │ │ ├── SEO.svelte │ │ ├── SettingsNavbar.svelte │ │ ├── Sidebar.svelte │ │ ├── SidebarContent.svelte │ │ ├── SidebarFooter.svelte │ │ ├── SidebarHeader.svelte │ │ ├── ThemeSwitcher.svelte │ │ ├── Wrapper.svelte │ │ └── ui │ │ │ ├── accordion │ │ │ ├── accordion-content.svelte │ │ │ ├── accordion-item.svelte │ │ │ ├── accordion-root.svelte │ │ │ ├── accordion-trigger.svelte │ │ │ └── index.ts │ │ │ ├── alert-dialog │ │ │ ├── alert-dialog-action.svelte │ │ │ ├── alert-dialog-cancel.svelte │ │ │ ├── alert-dialog-content.svelte │ │ │ ├── alert-dialog-description.svelte │ │ │ ├── alert-dialog-footer.svelte │ │ │ ├── alert-dialog-header.svelte │ │ │ ├── alert-dialog-overlay.svelte │ │ │ ├── alert-dialog-portal.svelte │ │ │ ├── alert-dialog-title.svelte │ │ │ ├── alert-dialog-trigger.svelte │ │ │ └── index.ts │ │ │ ├── alert │ │ │ ├── alert-description.svelte │ │ │ ├── alert-title.svelte │ │ │ ├── alert.svelte │ │ │ └── index.ts │ │ │ ├── aspect-ratio │ │ │ ├── aspect-ratio.svelte │ │ │ └── index.ts │ │ │ ├── avatar │ │ │ ├── avatar-fallback.svelte │ │ │ ├── avatar-image.svelte │ │ │ ├── avatar.svelte │ │ │ └── index.ts │ │ │ ├── badge │ │ │ ├── badge.svelte │ │ │ └── index.ts │ │ │ ├── breadcrumb │ │ │ ├── breadcrumb-ellipsis.svelte │ │ │ ├── breadcrumb-item.svelte │ │ │ ├── breadcrumb-link.svelte │ │ │ ├── breadcrumb-list.svelte │ │ │ ├── breadcrumb-page.svelte │ │ │ ├── breadcrumb-separator.svelte │ │ │ ├── breadcrumb.svelte │ │ │ └── index.ts │ │ │ ├── button │ │ │ ├── button.svelte │ │ │ └── index.ts │ │ │ ├── calendar │ │ │ ├── calendar-cell.svelte │ │ │ ├── calendar-day.svelte │ │ │ ├── calendar-grid-body.svelte │ │ │ ├── calendar-grid-head.svelte │ │ │ ├── calendar-grid-row.svelte │ │ │ ├── calendar-grid.svelte │ │ │ ├── calendar-head-cell.svelte │ │ │ ├── calendar-header.svelte │ │ │ ├── calendar-heading.svelte │ │ │ ├── calendar-months.svelte │ │ │ ├── calendar-next-button.svelte │ │ │ ├── calendar-prev-button.svelte │ │ │ ├── calendar.svelte │ │ │ └── index.ts │ │ │ ├── card │ │ │ ├── card-action.svelte │ │ │ ├── card-content.svelte │ │ │ ├── card-description.svelte │ │ │ ├── card-footer.svelte │ │ │ ├── card-header.svelte │ │ │ ├── card-title.svelte │ │ │ ├── card.svelte │ │ │ └── index.ts │ │ │ ├── carousel │ │ │ ├── carousel-content.svelte │ │ │ ├── carousel-item.svelte │ │ │ ├── carousel-next.svelte │ │ │ ├── carousel-previous.svelte │ │ │ ├── carousel.svelte │ │ │ ├── context.ts │ │ │ └── index.ts │ │ │ ├── chart │ │ │ ├── chart-container.svelte │ │ │ ├── chart-style.svelte │ │ │ ├── chart-tooltip.svelte │ │ │ ├── chart-utils.ts │ │ │ └── index.ts │ │ │ ├── checkbox │ │ │ ├── checkbox.svelte │ │ │ └── index.ts │ │ │ ├── collapsible │ │ │ ├── collapsible-content.svelte │ │ │ ├── collapsible-trigger.svelte │ │ │ ├── collapsible.svelte │ │ │ └── index.ts │ │ │ ├── command │ │ │ ├── command-dialog.svelte │ │ │ ├── command-empty.svelte │ │ │ ├── command-group.svelte │ │ │ ├── command-input.svelte │ │ │ ├── command-item.svelte │ │ │ ├── command-link-item.svelte │ │ │ ├── command-list.svelte │ │ │ ├── command-separator.svelte │ │ │ ├── command-shortcut.svelte │ │ │ ├── command.svelte │ │ │ └── index.ts │ │ │ ├── context-menu │ │ │ ├── context-menu-checkbox-item.svelte │ │ │ ├── context-menu-content.svelte │ │ │ ├── context-menu-group-heading.svelte │ │ │ ├── context-menu-group.svelte │ │ │ ├── context-menu-item.svelte │ │ │ ├── context-menu-label.svelte │ │ │ ├── context-menu-radio-group.svelte │ │ │ ├── context-menu-radio-item.svelte │ │ │ ├── context-menu-separator.svelte │ │ │ ├── context-menu-shortcut.svelte │ │ │ ├── context-menu-sub-content.svelte │ │ │ ├── context-menu-sub-trigger.svelte │ │ │ ├── context-menu-trigger.svelte │ │ │ └── index.ts │ │ │ ├── data-table │ │ │ ├── data-table.svelte.ts │ │ │ ├── flex-render.svelte │ │ │ ├── index.ts │ │ │ └── render-helpers.ts │ │ │ ├── dialog │ │ │ ├── dialog-close.svelte │ │ │ ├── dialog-content.svelte │ │ │ ├── dialog-description.svelte │ │ │ ├── dialog-footer.svelte │ │ │ ├── dialog-header.svelte │ │ │ ├── dialog-overlay.svelte │ │ │ ├── dialog-portal.svelte │ │ │ ├── dialog-title.svelte │ │ │ ├── dialog-trigger.svelte │ │ │ └── index.ts │ │ │ ├── drawer │ │ │ ├── drawer-close.svelte │ │ │ ├── drawer-content.svelte │ │ │ ├── drawer-description.svelte │ │ │ ├── drawer-footer.svelte │ │ │ ├── drawer-header.svelte │ │ │ ├── drawer-nested.svelte │ │ │ ├── drawer-overlay.svelte │ │ │ ├── drawer-title.svelte │ │ │ ├── drawer-trigger.svelte │ │ │ ├── drawer.svelte │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ ├── dropdown-menu-content.svelte │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ ├── dropdown-menu-group.svelte │ │ │ ├── dropdown-menu-item.svelte │ │ │ ├── dropdown-menu-label.svelte │ │ │ ├── dropdown-menu-radio-group.svelte │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ ├── dropdown-menu-separator.svelte │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ ├── dropdown-menu-trigger.svelte │ │ │ └── index.ts │ │ │ ├── form │ │ │ ├── form-button.svelte │ │ │ ├── form-checkbox.svelte │ │ │ ├── form-description.svelte │ │ │ ├── form-element-field.svelte │ │ │ ├── form-field-errors.svelte │ │ │ ├── form-field.svelte │ │ │ ├── form-fieldset.svelte │ │ │ ├── form-input.svelte │ │ │ ├── form-item.svelte │ │ │ ├── form-label.svelte │ │ │ ├── form-legend.svelte │ │ │ ├── form-native-select.svelte │ │ │ ├── form-radio-group.svelte │ │ │ ├── form-select-trigger.svelte │ │ │ ├── form-select.svelte │ │ │ ├── form-switch.svelte │ │ │ ├── form-textarea.svelte │ │ │ ├── form-validation.svelte │ │ │ └── index.ts │ │ │ ├── hover-card │ │ │ ├── hover-card-content.svelte │ │ │ ├── hover-card-trigger.svelte │ │ │ └── index.ts │ │ │ ├── input-otp │ │ │ ├── index.ts │ │ │ ├── input-otp-group.svelte │ │ │ ├── input-otp-separator.svelte │ │ │ ├── input-otp-slot.svelte │ │ │ └── input-otp.svelte │ │ │ ├── input │ │ │ ├── index.ts │ │ │ └── input.svelte │ │ │ ├── label │ │ │ ├── index.ts │ │ │ └── label.svelte │ │ │ ├── menubar │ │ │ ├── index.ts │ │ │ ├── menubar-checkbox-item.svelte │ │ │ ├── menubar-content.svelte │ │ │ ├── menubar-group-heading.svelte │ │ │ ├── menubar-group.svelte │ │ │ ├── menubar-item.svelte │ │ │ ├── menubar-label.svelte │ │ │ ├── menubar-radio-item.svelte │ │ │ ├── menubar-separator.svelte │ │ │ ├── menubar-shortcut.svelte │ │ │ ├── menubar-sub-content.svelte │ │ │ ├── menubar-sub-trigger.svelte │ │ │ ├── menubar-trigger.svelte │ │ │ └── menubar.svelte │ │ │ ├── pagination │ │ │ ├── index.ts │ │ │ ├── pagination-content.svelte │ │ │ ├── pagination-ellipsis.svelte │ │ │ ├── pagination-item.svelte │ │ │ ├── pagination-link.svelte │ │ │ ├── pagination-next-button.svelte │ │ │ ├── pagination-prev-button.svelte │ │ │ └── pagination.svelte │ │ │ ├── popover │ │ │ ├── index.ts │ │ │ ├── popover-content.svelte │ │ │ └── popover-trigger.svelte │ │ │ ├── progress │ │ │ ├── index.ts │ │ │ └── progress.svelte │ │ │ ├── radio-group │ │ │ ├── index.ts │ │ │ ├── radio-group-item.svelte │ │ │ └── radio-group.svelte │ │ │ ├── range-calendar │ │ │ ├── index.ts │ │ │ ├── range-calendar-cell.svelte │ │ │ ├── range-calendar-day.svelte │ │ │ ├── range-calendar-grid-row.svelte │ │ │ ├── range-calendar-grid.svelte │ │ │ ├── range-calendar-head-cell.svelte │ │ │ ├── range-calendar-header.svelte │ │ │ ├── range-calendar-heading.svelte │ │ │ ├── range-calendar-months.svelte │ │ │ ├── range-calendar-next-button.svelte │ │ │ ├── range-calendar-prev-button.svelte │ │ │ └── range-calendar.svelte │ │ │ ├── resizable │ │ │ ├── index.ts │ │ │ ├── resizable-handle.svelte │ │ │ └── resizable-pane-group.svelte │ │ │ ├── scroll-area │ │ │ ├── index.ts │ │ │ ├── scroll-area-scrollbar.svelte │ │ │ └── scroll-area.svelte │ │ │ ├── select │ │ │ ├── index.ts │ │ │ ├── select-content.svelte │ │ │ ├── select-group.svelte │ │ │ ├── select-item.svelte │ │ │ ├── select-label.svelte │ │ │ ├── select-scroll-down-button.svelte │ │ │ ├── select-scroll-up-button.svelte │ │ │ ├── select-separator.svelte │ │ │ └── select-trigger.svelte │ │ │ ├── separator │ │ │ ├── index.ts │ │ │ └── separator.svelte │ │ │ ├── sheet │ │ │ ├── index.ts │ │ │ ├── sheet-close.svelte │ │ │ ├── sheet-content.svelte │ │ │ ├── sheet-description.svelte │ │ │ ├── sheet-footer.svelte │ │ │ ├── sheet-header.svelte │ │ │ ├── sheet-overlay.svelte │ │ │ ├── sheet-title.svelte │ │ │ └── sheet-trigger.svelte │ │ │ ├── sidebar │ │ │ ├── constants.ts │ │ │ ├── context.svelte.ts │ │ │ ├── index.ts │ │ │ ├── sidebar-content.svelte │ │ │ ├── sidebar-footer.svelte │ │ │ ├── sidebar-group-action.svelte │ │ │ ├── sidebar-group-content.svelte │ │ │ ├── sidebar-group-label.svelte │ │ │ ├── sidebar-group.svelte │ │ │ ├── sidebar-header.svelte │ │ │ ├── sidebar-input.svelte │ │ │ ├── sidebar-inset.svelte │ │ │ ├── sidebar-menu-action.svelte │ │ │ ├── sidebar-menu-badge.svelte │ │ │ ├── sidebar-menu-button.svelte │ │ │ ├── sidebar-menu-item.svelte │ │ │ ├── sidebar-menu-skeleton.svelte │ │ │ ├── sidebar-menu-sub-button.svelte │ │ │ ├── sidebar-menu-sub-item.svelte │ │ │ ├── sidebar-menu-sub.svelte │ │ │ ├── sidebar-menu.svelte │ │ │ ├── sidebar-provider.svelte │ │ │ ├── sidebar-rail.svelte │ │ │ ├── sidebar-separator.svelte │ │ │ ├── sidebar-trigger.svelte │ │ │ └── sidebar.svelte │ │ │ ├── skeleton │ │ │ ├── index.ts │ │ │ └── skeleton.svelte │ │ │ ├── slider │ │ │ ├── index.ts │ │ │ └── slider.svelte │ │ │ ├── sonner │ │ │ ├── index.ts │ │ │ └── sonner.svelte │ │ │ ├── switch │ │ │ ├── index.ts │ │ │ └── switch.svelte │ │ │ ├── table │ │ │ ├── index.ts │ │ │ ├── table-body.svelte │ │ │ ├── table-caption.svelte │ │ │ ├── table-cell.svelte │ │ │ ├── table-footer.svelte │ │ │ ├── table-head.svelte │ │ │ ├── table-header.svelte │ │ │ ├── table-row.svelte │ │ │ └── table.svelte │ │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── tabs-content.svelte │ │ │ ├── tabs-list.svelte │ │ │ ├── tabs-trigger.svelte │ │ │ └── tabs.svelte │ │ │ ├── textarea │ │ │ ├── index.ts │ │ │ └── textarea.svelte │ │ │ ├── toggle-group │ │ │ ├── index.ts │ │ │ ├── toggle-group-item.svelte │ │ │ └── toggle-group.svelte │ │ │ ├── toggle │ │ │ ├── index.ts │ │ │ └── toggle.svelte │ │ │ └── tooltip │ │ │ ├── index.ts │ │ │ ├── tooltip-content.svelte │ │ │ └── tooltip-trigger.svelte │ ├── db │ │ ├── clear.ts │ │ ├── factories │ │ │ ├── teamFactory.ts │ │ │ ├── userFactory.ts │ │ │ └── usersTeamsFactory.ts │ │ ├── migrations │ │ │ ├── 0000_lame_jane_foster.sql │ │ │ └── meta │ │ │ │ ├── 0000_snapshot.json │ │ │ │ └── _journal.json │ │ ├── models │ │ │ ├── invite.ts │ │ │ ├── session.ts │ │ │ ├── team.ts │ │ │ ├── token.ts │ │ │ └── user.ts │ │ ├── queries │ │ │ ├── invite.ts │ │ │ └── team.ts │ │ └── seed.ts │ ├── hooks │ │ └── is-mobile.svelte.ts │ ├── server │ │ ├── auth.ts │ │ ├── database.ts │ │ ├── oauth.ts │ │ └── storage.ts │ ├── utils │ │ ├── helpers │ │ │ ├── forms.ts │ │ │ ├── generate.ts │ │ │ ├── password.ts │ │ │ └── uploadFile.svelte.ts │ │ ├── mail │ │ │ ├── mailer.ts │ │ │ ├── templates │ │ │ │ ├── ResetPassword.svelte │ │ │ │ ├── TeamInvite.svelte │ │ │ │ ├── Welcome.svelte │ │ │ │ └── mailTheme.ts │ │ │ └── types.ts │ │ ├── messages.json │ │ ├── themes.ts │ │ └── utils.ts │ └── validations │ │ ├── auth.ts │ │ ├── files.ts │ │ └── team.ts ├── routes │ ├── (auth) │ │ ├── +layout.svelte │ │ ├── login │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── google │ │ │ │ ├── +server.ts │ │ │ │ └── callback │ │ │ │ └── +server.ts │ │ ├── logout │ │ │ └── +server.ts │ │ ├── register │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ └── reset-password │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── update │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── (dashboard) │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── dashboard │ │ │ └── +page.svelte │ │ └── settings │ │ │ ├── +layout.svelte │ │ │ ├── profile │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ │ └── teams │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── [teamPublicId] │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +page.server.ts │ ├── +page.svelte │ └── api │ │ ├── invites │ │ └── +server.ts │ │ └── upload │ │ └── +server.ts └── styles │ └── app.css ├── static ├── favicon.png ├── fonts │ ├── Montserrat-VariableFont.ttf │ └── Sora-VariableFont.ttf └── robots.txt ├── svelte.config.js ├── tests ├── e2e │ └── homepage.test.ts └── unit │ └── demo.spec.ts ├── tsconfig.json ├── vite.config.ts └── vitest-setup-client.ts /.env.example: -------------------------------------------------------------------------------- 1 | # General 2 | PUBLIC_BASE_URL="http://localhost:5173" 3 | 4 | # Database (LOCAL) 5 | LOCAL_DATABASE_URL="file:sveltekit_omakase_local.db" 6 | 7 | # Database (PRODUCTION) 8 | DATABASE_URL="libsql://db-name.turso.io" 9 | DATABASE_AUTH_TOKEN="" 10 | 11 | # Google 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="" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | 9 | jobs: 10 | lint-and-test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | # Set up pnpm 17 | - name: Set up pnpm 18 | uses: pnpm/action-setup@v4 19 | with: 20 | version: 9 # Specify pnpm version if needed, otherwise latest 21 | 22 | # Set up Node.js 23 | - name: Set up Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 'lts/*' # Using the latest Node.js LTS version 27 | cache: 'pnpm' 28 | 29 | # Install dependencies 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | # Run linter 34 | - name: Run linter 35 | run: pnpm lint 36 | 37 | # Run unit tests 38 | - name: Run unit tests 39 | run: pnpm test:unit 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | dev.db 10 | /test-results 11 | /.vscode 12 | *.db -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | /src/lib/db/migrations 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 120, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { "parser": "svelte" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "style": "new-york", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/styles/app.css", 7 | "baseColor": "slate" 8 | }, 9 | "aliases": { 10 | "components": "$components", 11 | "utils": "$lib/utils/utils", 12 | "ui": "$lib/components/ui", 13 | "hooks": "$lib/hooks" 14 | }, 15 | "typescript": true, 16 | "registry": "https://next.shadcn-svelte.com/registry" 17 | } 18 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit'; 2 | import 'dotenv/config'; 3 | 4 | const localConfig = { 5 | dialect: 'turso', 6 | schema: './src/lib/db/models/*', 7 | out: './src/lib/db/migrations', 8 | breakpoints: true, 9 | casing: 'snake_case', 10 | verbose: true, 11 | dbCredentials: { 12 | url: process.env.LOCAL_DATABASE_URL || '' 13 | } 14 | } as Config; 15 | 16 | const remoteConfig = { 17 | dialect: 'turso', 18 | schema: './src/lib/db/models/*', 19 | out: './src/lib/db/migrations', 20 | breakpoints: true, 21 | casing: 'snake_case', 22 | verbose: true, 23 | dbCredentials: { 24 | url: process.env.DATABASE_URL || '', 25 | authToken: process.env.DATABASE_AUTH_TOKEN || '' 26 | } 27 | } as Config; 28 | 29 | export default process.env.NODE_ENV === 'production' ? remoteConfig : localConfig; 30 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | import svelteConfig from './svelte.config.js'; 9 | 10 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 11 | 12 | export default ts.config( 13 | includeIgnoreFile(gitignorePath), 14 | js.configs.recommended, 15 | ...ts.configs.recommended, 16 | ...svelte.configs.recommended, 17 | prettier, 18 | ...svelte.configs.prettier, 19 | { 20 | languageOptions: { 21 | globals: { ...globals.browser, ...globals.node } 22 | }, 23 | rules: { 'no-undef': 'off' } 24 | }, 25 | { 26 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 27 | languageOptions: { 28 | parserOptions: { 29 | projectService: true, 30 | extraFileExtensions: ['.svelte'], 31 | parser: ts.parser, 32 | svelteConfig 33 | } 34 | } 35 | }, 36 | { 37 | ignores: ['*.env', 'node_modules/', 'build/', '.svelte-kit/', 'src/lib/components/ui/**'] 38 | } 39 | ); 40 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | webServer: { 5 | command: 'pnpm build && pnpm preview', 6 | port: 4173 7 | }, 8 | testDir: 'tests/e2e' 9 | }); 10 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace App { 3 | interface Locals { 4 | user: import('$lib/server/auth').SessionValidationResult['user']; 5 | session: import('$lib/server/auth').SessionValidationResult['session']; 6 | } 7 | 8 | interface PageData { 9 | metadata: { 10 | title: string; 11 | description: string; 12 | image: string; 13 | url: string; 14 | breadcrumbs: { 15 | title: string; 16 | href: string; 17 | }[]; 18 | }; 19 | } 20 | } 21 | } 22 | 23 | export {}; 24 | -------------------------------------------------------------------------------- /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/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | import * as auth from '$lib/server/auth'; 3 | 4 | const handleAuth: Handle = async ({ event, resolve }) => { 5 | const sessionToken = event.cookies.get(auth.sessionCookieName); 6 | if (!sessionToken) { 7 | event.locals.user = null; 8 | event.locals.session = null; 9 | return resolve(event); 10 | } 11 | 12 | const { session, user } = await auth.validateSessionToken(sessionToken); 13 | if (session) { 14 | auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); 15 | } else { 16 | auth.deleteSessionTokenCookie(event); 17 | } 18 | 19 | event.locals.user = user; 20 | event.locals.session = session; 21 | 22 | return resolve(event); 23 | }; 24 | 25 | export const handle: Handle = handleAuth; 26 | -------------------------------------------------------------------------------- /src/lib/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/8de5357d79ffdc52acae042c0731f3b6fda97135/src/lib/assets/avatar.png -------------------------------------------------------------------------------- /src/lib/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/8de5357d79ffdc52acae042c0731f3b6fda97135/src/lib/assets/logo.png -------------------------------------------------------------------------------- /src/lib/assets/meta_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/8de5357d79ffdc52acae042c0731f3b6fda97135/src/lib/assets/meta_image.png -------------------------------------------------------------------------------- /src/lib/components/PlaceholderPattern.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/components/SidebarHeader.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | {#snippet child({ props })} 13 | 14 |
17 | SvelteKit Omakase 18 |
19 |
20 | SvelteKit Omakase 21 |
22 |
23 | {/snippet} 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/lib/components/Wrapper.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | {@render children?.()} 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 |
23 | {@render children?.()} 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-item.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-root.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './accordion-root.svelte'; 2 | import Content from './accordion-content.svelte'; 3 | import Item from './accordion-item.svelte'; 4 | import Trigger from './accordion-trigger.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 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-action.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {@render children?.()} 15 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; 2 | import Trigger from './alert-dialog-trigger.svelte'; 3 | import Title from './alert-dialog-title.svelte'; 4 | import Action from './alert-dialog-action.svelte'; 5 | import Cancel from './alert-dialog-cancel.svelte'; 6 | import Footer from './alert-dialog-footer.svelte'; 7 | import Header from './alert-dialog-header.svelte'; 8 | import Overlay from './alert-dialog-overlay.svelte'; 9 | import Content from './alert-dialog-content.svelte'; 10 | import Description from './alert-dialog-description.svelte'; 11 | 12 | const Root = AlertDialogPrimitive.Root; 13 | const Portal = AlertDialogPrimitive.Portal; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Action, 19 | Cancel, 20 | Portal, 21 | Footer, 22 | Header, 23 | Trigger, 24 | Overlay, 25 | Content, 26 | Description, 27 | // 28 | Root as AlertDialog, 29 | Title as AlertDialogTitle, 30 | Action as AlertDialogAction, 31 | Cancel as AlertDialogCancel, 32 | Portal as AlertDialogPortal, 33 | Footer as AlertDialogFooter, 34 | Header as AlertDialogHeader, 35 | Trigger as AlertDialogTrigger, 36 | Overlay as AlertDialogOverlay, 37 | Content as AlertDialogContent, 38 | Description as AlertDialogDescription 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-description.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
22 | {@render children?.()} 23 |
24 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-title.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './alert.svelte'; 2 | import Description from './alert-description.svelte'; 3 | import Title from './alert-title.svelte'; 4 | export { alertVariants, type AlertVariant } from './alert.svelte'; 5 | 6 | export { 7 | Root, 8 | Description, 9 | Title, 10 | // 11 | Root as Alert, 12 | Description as AlertDescription, 13 | Title as AlertTitle 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/aspect-ratio/aspect-ratio.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/aspect-ratio/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './aspect-ratio.svelte'; 2 | 3 | export { Root, Root as AspectRatio }; 4 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar-fallback.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar-image.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './avatar.svelte'; 2 | import Image from './avatar-image.svelte'; 3 | import Fallback from './avatar-fallback.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/badge/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Badge } from './badge.svelte'; 2 | export { badgeVariants, type BadgeVariant } from './badge.svelte'; 3 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-item.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
  • 14 | {@render children?.()} 15 |
  • 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-link.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | {#if child} 26 | {@render child({ props: attrs })} 27 | {:else} 28 | 29 | {@render children?.()} 30 | 31 | {/if} 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-list.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
      14 | {@render children?.()} 15 |
    16 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | {@render children?.()} 23 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './breadcrumb.svelte'; 2 | import Ellipsis from './breadcrumb-ellipsis.svelte'; 3 | import Item from './breadcrumb-item.svelte'; 4 | import Separator from './breadcrumb-separator.svelte'; 5 | import Link from './breadcrumb-link.svelte'; 6 | import List from './breadcrumb-list.svelte'; 7 | import Page from './breadcrumb-page.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/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root, { type ButtonProps, type ButtonSize, type ButtonVariant, buttonVariants } from './button.svelte'; 2 | 3 | export { 4 | Root, 5 | type ButtonProps as Props, 6 | // 7 | Root as Button, 8 | buttonVariants, 9 | type ButtonProps, 10 | type ButtonSize, 11 | type ButtonVariant 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-cell.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid-body.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid-head.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid-row.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-grid.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-head-cell.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-header.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-heading.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-months.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    18 | {@render children?.()} 19 |
    20 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-next-button.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#snippet Fallback()} 11 | 12 | {/snippet} 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/calendar-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#snippet Fallback()} 11 | 12 | {/snippet} 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/calendar/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './calendar.svelte'; 2 | import Cell from './calendar-cell.svelte'; 3 | import Day from './calendar-day.svelte'; 4 | import Grid from './calendar-grid.svelte'; 5 | import Header from './calendar-header.svelte'; 6 | import Months from './calendar-months.svelte'; 7 | import GridRow from './calendar-grid-row.svelte'; 8 | import Heading from './calendar-heading.svelte'; 9 | import GridBody from './calendar-grid-body.svelte'; 10 | import GridHead from './calendar-grid-head.svelte'; 11 | import HeadCell from './calendar-head-cell.svelte'; 12 | import NextButton from './calendar-next-button.svelte'; 13 | import PrevButton from './calendar-prev-button.svelte'; 14 | 15 | export { 16 | Day, 17 | Cell, 18 | Grid, 19 | Header, 20 | Months, 21 | GridRow, 22 | Heading, 23 | GridBody, 24 | GridHead, 25 | HeadCell, 26 | NextButton, 27 | PrevButton, 28 | // 29 | Root as Calendar 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-action.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    14 | {@render children?.()} 15 |
    16 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |

    14 | {@render children?.()} 15 |

    16 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    22 | {@render children?.()} 23 |
    24 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    14 | {@render children?.()} 15 |
    16 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './card.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 Action from './card-action.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/carousel/carousel-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
    31 |
    37 | {@render children?.()} 38 |
    39 |
    40 | -------------------------------------------------------------------------------- /src/lib/components/ui/carousel/carousel-item.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    25 | {@render children?.()} 26 |
    27 | -------------------------------------------------------------------------------- /src/lib/components/ui/carousel/carousel-next.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /src/lib/components/ui/carousel/carousel-previous.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /src/lib/components/ui/carousel/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './carousel.svelte'; 2 | import Content from './carousel-content.svelte'; 3 | import Item from './carousel-item.svelte'; 4 | import Previous from './carousel-previous.svelte'; 5 | import Next from './carousel-next.svelte'; 6 | 7 | export { 8 | Root, 9 | Content, 10 | Item, 11 | Previous, 12 | Next, 13 | // 14 | Root as Carousel, 15 | Content as CarouselContent, 16 | Item as CarouselItem, 17 | Previous as CarouselPrevious, 18 | Next as CarouselNext 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/chart/chart-style.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if colorConfig && colorConfig.length} 15 | {@const themeContents = Object.entries(THEMES) 16 | .map( 17 | ([theme, prefix]) => ` 18 | ${prefix} [data-chart=${id}] { 19 | ${colorConfig 20 | .map(([key, itemConfig]) => { 21 | const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color; 22 | return color ? ` --color-${key}: ${color};` : null; 23 | }) 24 | .join('\n')} 25 | } 26 | ` 27 | ) 28 | .join('\n')} 29 | 30 | {#key id} 31 | 32 | {@html `${styleOpen} 33 | ${themeContents} 34 | ${styleClose}`} 35 | {/key} 36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/chart/index.ts: -------------------------------------------------------------------------------- 1 | import ChartContainer from './chart-container.svelte'; 2 | import ChartTooltip from './chart-tooltip.svelte'; 3 | 4 | export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js'; 5 | 6 | export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip }; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './checkbox.svelte'; 2 | export { 3 | Root, 4 | // 5 | Root as Checkbox 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/collapsible/collapsible-content.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/collapsible/collapsible-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/collapsible/collapsible.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/collapsible/index.ts: -------------------------------------------------------------------------------- 1 | import { Collapsible as CollapsiblePrimitive } from 'bits-ui'; 2 | 3 | const Root = CollapsiblePrimitive.Root; 4 | const Trigger = CollapsiblePrimitive.Trigger; 5 | const Content = CollapsiblePrimitive.Content; 6 | 7 | export { 8 | Root, 9 | Content, 10 | Trigger, 11 | // 12 | Root as Collapsible, 13 | Content as CollapsibleContent, 14 | Trigger as CollapsibleTrigger 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-empty.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-group.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | {#if heading} 25 | 26 | {heading} 27 | 28 | {/if} 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 | 16 | 26 |
    27 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-item.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-link-item.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-list.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-separator.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/command.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/command/index.ts: -------------------------------------------------------------------------------- 1 | import { Command as CommandPrimitive } from 'bits-ui'; 2 | 3 | import Root from './command.svelte'; 4 | import Dialog from './command-dialog.svelte'; 5 | import Empty from './command-empty.svelte'; 6 | import Group from './command-group.svelte'; 7 | import Item from './command-item.svelte'; 8 | import Input from './command-input.svelte'; 9 | import List from './command-list.svelte'; 10 | import Separator from './command-separator.svelte'; 11 | import Shortcut from './command-shortcut.svelte'; 12 | import LinkItem from './command-link-item.svelte'; 13 | 14 | const Loading = CommandPrimitive.Loading; 15 | 16 | export { 17 | Root, 18 | Dialog, 19 | Empty, 20 | Group, 21 | Item, 22 | LinkItem, 23 | Input, 24 | List, 25 | Separator, 26 | Shortcut, 27 | Loading, 28 | // 29 | Root as Command, 30 | Dialog as CommandDialog, 31 | Empty as CommandEmpty, 32 | Group as CommandGroup, 33 | Item as CommandItem, 34 | LinkItem as CommandLinkItem, 35 | Input as CommandInput, 36 | List as CommandList, 37 | Separator as CommandSeparator, 38 | Shortcut as CommandShortcut, 39 | Loading as CommandLoading 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    23 | {@render children?.()} 24 |
    25 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | {@render children?.()} 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/lib/components/ui/context-menu/context-menu-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/data-table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FlexRender } from './flex-render.svelte'; 2 | export { renderComponent, renderSnippet } from './render-helpers.js'; 3 | export { createSvelteTable } from './data-table.svelte.js'; 4 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-close.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    19 | {@render children?.()} 20 |
    21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {@render children?.()} 15 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from 'bits-ui'; 2 | 3 | import Title from './dialog-title.svelte'; 4 | import Footer from './dialog-footer.svelte'; 5 | import Header from './dialog-header.svelte'; 6 | import Overlay from './dialog-overlay.svelte'; 7 | import Content from './dialog-content.svelte'; 8 | import Description from './dialog-description.svelte'; 9 | import Trigger from './dialog-trigger.svelte'; 10 | import Close from './dialog-close.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/drawer/drawer-close.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer-description.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer-footer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    14 | {@render children?.()} 15 |
    16 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer-header.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    14 | {@render children?.()} 15 |
    16 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer-nested.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer-overlay.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer-title.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/drawer.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/drawer/index.ts: -------------------------------------------------------------------------------- 1 | import { Drawer as DrawerPrimitive } from 'vaul-svelte'; 2 | 3 | import Root from './drawer.svelte'; 4 | import Content from './drawer-content.svelte'; 5 | import Description from './drawer-description.svelte'; 6 | import Overlay from './drawer-overlay.svelte'; 7 | import Footer from './drawer-footer.svelte'; 8 | import Header from './drawer-header.svelte'; 9 | import Title from './drawer-title.svelte'; 10 | import NestedRoot from './drawer-nested.svelte'; 11 | import Close from './drawer-close.svelte'; 12 | import Trigger from './drawer-trigger.svelte'; 13 | 14 | const Portal: typeof DrawerPrimitive.Portal = DrawerPrimitive.Portal; 15 | 16 | export { 17 | Root, 18 | NestedRoot, 19 | Content, 20 | Description, 21 | Overlay, 22 | Footer, 23 | Header, 24 | Title, 25 | Trigger, 26 | Portal, 27 | Close, 28 | 29 | // 30 | Root as Drawer, 31 | NestedRoot as DrawerNestedRoot, 32 | Content as DrawerContent, 33 | Description as DrawerDescription, 34 | Overlay as DrawerOverlay, 35 | Footer as DrawerFooter, 36 | Header as DrawerHeader, 37 | Title as DrawerTitle, 38 | Trigger as DrawerTrigger, 39 | Portal as DrawerPortal, 40 | Close as DrawerClose 41 | }; 42 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    23 | {@render children?.()} 24 |
    25 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | {@render children?.()} 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-button.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-checkbox.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | { 24 | onCheckedChange?.(v); 25 | setValue(v); 26 | }} 27 | {...rest_1} 28 | on:click 29 | on:keydown 30 | /> 31 | 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-element-field.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 23 | {#snippet children({ constraints, errors, tainted, value })} 24 |
    25 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 26 |
    27 | {/snippet} 28 |
    29 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-field-errors.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | {#snippet children({ errors, errorProps })} 18 | {#if childrenProp} 19 | {@render childrenProp({ errors, errorProps })} 20 | {:else} 21 | {#each errors as error (error)} 22 |
    {error}
    23 | {/each} 24 | {/if} 25 | {/snippet} 26 |
    27 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-field.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 23 | {#snippet children({ constraints, errors, tainted, value })} 24 |
    25 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 26 |
    27 | {/snippet} 28 |
    29 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-fieldset.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-input.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    16 | {@render children?.()} 17 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-label.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | {#snippet child({ props })} 16 | 19 | {/snippet} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-legend.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-native-select.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
    20 | 24 | {@render children?.()} 25 | 26 | 27 |
    28 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | { 21 | onValueChange?.(v); 22 | setValue(v); 23 | }} 24 | {...rest} 25 | > 26 | {@render children?.()} 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | {@render children?.()} 23 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-select.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | { 19 | onSelectedChange?.(v); 20 | setValue(v ? v.value : undefined); 21 | }} 22 | {...rest} 23 | > 24 | {@render children?.()} 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-switch.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | { 22 | onCheckedChange?.(v); 23 | setValue(v); 24 | }} 25 | {...rest} 26 | on:click 27 | on:keydown 28 | /> 29 | 30 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-textarea.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle-group/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './toggle-group.svelte'; 2 | import Item from './toggle-group-item.svelte'; 3 | 4 | export { 5 | Root, 6 | Item, 7 | // 8 | Root as ToggleGroup, 9 | Item as ToggleGroupItem 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle-group/toggle-group-item.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/toggle/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './toggle.svelte'; 2 | export { toggleVariants, type ToggleSize, type ToggleVariant, type ToggleVariants } from './toggle.svelte'; 3 | 4 | export { 5 | Root, 6 | // 7 | Root as Toggle 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from 'bits-ui'; 2 | import Trigger from './tooltip-trigger.svelte'; 3 | import Content from './tooltip-content.svelte'; 4 | 5 | const Root = TooltipPrimitive.Root; 6 | const Provider = TooltipPrimitive.Provider; 7 | const Portal = TooltipPrimitive.Portal; 8 | 9 | export { 10 | Root, 11 | Trigger, 12 | Content, 13 | Provider, 14 | Portal, 15 | // 16 | Root as Tooltip, 17 | Content as TooltipContent, 18 | Trigger as TooltipTrigger, 19 | Provider as TooltipProvider, 20 | Portal as TooltipPortal 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/db/factories/teamFactory.ts: -------------------------------------------------------------------------------- 1 | import type { Team } from '$lib/db/models/team'; 2 | import { faker } from '@faker-js/faker'; 3 | import { generateNanoId } from '$lib/utils/helpers/generate'; 4 | 5 | export const teamFactory = async (instances: number): Promise => { 6 | return Array.from({ length: instances }, (_, index) => ({ 7 | id: index + 1, 8 | publicId: generateNanoId(), 9 | name: faker.company.name(), 10 | avatar: faker.image.avatar(), 11 | createdAt: new Date(), 12 | updatedAt: new Date() 13 | })); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/db/factories/userFactory.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '$lib/db/models/user'; 2 | import { faker } from '@faker-js/faker'; 3 | import { hashPassword } from '$lib/utils/helpers/password'; 4 | import { generateNanoId } from '$lib/utils/helpers/generate'; 5 | 6 | export const userFactory = async (instances: number): Promise => { 7 | const hashedPassword = await hashPassword('password1234'); 8 | 9 | return Array.from({ length: instances }, (_, index) => ({ 10 | id: index + 1, 11 | publicId: generateNanoId(), 12 | email: faker.internet.email(), 13 | googleId: null, 14 | firstName: faker.person.firstName(), 15 | lastName: faker.person.lastName(), 16 | hashedPassword, 17 | avatar: faker.image.avatar(), 18 | admin: false, 19 | createdAt: new Date(), 20 | updatedAt: new Date() 21 | })); 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/db/factories/usersTeamsFactory.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '$lib/db/models/user'; 2 | import type { Team } from '$lib/db/models/team'; 3 | import { faker } from '@faker-js/faker'; 4 | 5 | export const usersTeamsFactory = async (users: User[], teams: Team[]) => { 6 | return Array.from({ length: users.length }, (_, index) => ({ 7 | userId: users[index].id, 8 | teamId: teams[index].id, 9 | role: faker.helpers.arrayElement(['admin', 'member']) 10 | })); 11 | }; 12 | -------------------------------------------------------------------------------- /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": 1748536656388, 9 | "tag": "0000_lame_jane_foster", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /src/lib/db/models/session.ts: -------------------------------------------------------------------------------- 1 | import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'; 2 | 3 | import { User } from './user'; 4 | 5 | export const Session = sqliteTable('sessions', { 6 | id: text().notNull().primaryKey(), 7 | userId: integer('user_id') 8 | .notNull() 9 | .references(() => User.id), 10 | expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull() 11 | }); 12 | 13 | export type Session = typeof Session.$inferSelect; 14 | -------------------------------------------------------------------------------- /src/lib/db/models/token.ts: -------------------------------------------------------------------------------- 1 | import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'; 2 | import { relations } from 'drizzle-orm'; 3 | import { generateToken } from '../../utils/helpers/generate'; 4 | 5 | import { User } from './user'; 6 | 7 | export const Token = sqliteTable('tokens', { 8 | id: integer({ mode: 'number' }).primaryKey({ autoIncrement: true }), 9 | key: text() 10 | .notNull() 11 | .unique() 12 | .$default(() => generateToken()), 13 | userId: integer('user_id') 14 | .references(() => User.id, { onDelete: 'cascade' }) 15 | .unique(), 16 | expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull() 17 | }); 18 | 19 | export type Token = typeof Token.$inferSelect; 20 | 21 | export const TokenRelations = relations(Token, ({ one }) => ({ 22 | user: one(User, { 23 | fields: [Token.userId], 24 | references: [User.id] 25 | }) 26 | })); 27 | -------------------------------------------------------------------------------- /src/lib/db/models/user.ts: -------------------------------------------------------------------------------- 1 | import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'; 2 | import { sql, relations } from 'drizzle-orm'; 3 | 4 | import { UsersTeams } from './team'; 5 | 6 | export const User = sqliteTable('users', { 7 | id: integer().notNull().primaryKey({ autoIncrement: true }), 8 | publicId: text('public_id').notNull(), 9 | email: text().unique(), 10 | googleId: integer('google_id').unique(), 11 | firstName: text('first_name').notNull(), 12 | lastName: text('last_name').notNull(), 13 | hashedPassword: text('hashed_password'), 14 | avatar: text(), 15 | admin: integer({ mode: 'boolean' }).notNull().default(false), 16 | createdAt: integer('created_at', { mode: 'timestamp_ms' }) 17 | .notNull() 18 | .default(sql`(CURRENT_TIMESTAMP)`), 19 | updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) 20 | }); 21 | 22 | export const UserRelations = relations(User, ({ many }) => ({ 23 | userTeams: many(UsersTeams) 24 | })); 25 | 26 | export type User = typeof User.$inferSelect; 27 | -------------------------------------------------------------------------------- /src/lib/db/queries/invite.ts: -------------------------------------------------------------------------------- 1 | import { and } from 'drizzle-orm'; 2 | // Types 3 | import type { PreparedQueryConfig } from 'drizzle-orm/sqlite-core'; 4 | import type { SQLitePreparedQuery } from 'drizzle-orm/sqlite-core'; 5 | 6 | // Utils 7 | import { eq, sql } from 'drizzle-orm'; 8 | 9 | // Database 10 | import db from '$lib/server/database'; 11 | import { Invite } from '$models/invite'; 12 | 13 | // SELECT * FROM invites WHERE email = ? 14 | export const getUserPendingInvitesByEmail = db.query.Invite.findMany({ 15 | where: and(eq(Invite.email, sql.placeholder('email')), eq(Invite.status, 'pending')), 16 | with: { 17 | team: { 18 | columns: { 19 | id: true, 20 | name: true 21 | } 22 | } 23 | }, 24 | orderBy: (invites, { desc }) => [desc(invites.createdAt)] 25 | }); 26 | 27 | export const getUserPendingInvitesByEmailQuery: SQLitePreparedQuery = 28 | getUserPendingInvitesByEmail.prepare(); 29 | export type GetUserPendingInvitesByEmail = Awaited>; 30 | -------------------------------------------------------------------------------- /src/lib/db/seed.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import db from '$lib/server/database'; 3 | 4 | import { User } from '$models/user'; 5 | import { Team, UsersTeams } from '$models/team'; 6 | 7 | import { userFactory } from './factories/userFactory'; 8 | import { teamFactory } from './factories/teamFactory'; 9 | import { usersTeamsFactory } from './factories/usersTeamsFactory'; 10 | 11 | async function seed() { 12 | config(); 13 | 14 | if (process.env.NODE_ENV === 'production') { 15 | console.error('Seed script should not be run in production!'); 16 | process.exit(1); 17 | } 18 | 19 | console.log(`🌱 Seeding the database with 100 users and 100 teams...`); 20 | 21 | const users = await userFactory(100); 22 | const teams = await teamFactory(100); 23 | const usersTeams = await usersTeamsFactory(users, teams); 24 | 25 | try { 26 | await db.batch([ 27 | db.insert(User).values(users), 28 | db.insert(Team).values(teams), 29 | db.insert(UsersTeams).values(usersTeams) 30 | ]); 31 | 32 | console.log('Database seeded.'); 33 | } catch (error) { 34 | console.error('Error seeding the database:', error); 35 | process.exit(1); 36 | } 37 | } 38 | 39 | seed(); 40 | -------------------------------------------------------------------------------- /src/lib/hooks/is-mobile.svelte.ts: -------------------------------------------------------------------------------- 1 | import { MediaQuery } from 'svelte/reactivity'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export class IsMobile extends MediaQuery { 6 | constructor() { 7 | super(`max-width: ${MOBILE_BREAKPOINT - 1}px`); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/server/oauth.ts: -------------------------------------------------------------------------------- 1 | import { PUBLIC_BASE_URL } from '$env/static/public'; 2 | import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from '$env/static/private'; 3 | import { Google } from 'arctic'; 4 | 5 | if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) { 6 | throw new Error('GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are required for Google OAuth'); 7 | } 8 | 9 | export const google = new Google(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, `${PUBLIC_BASE_URL}/login/google/callback`); 10 | -------------------------------------------------------------------------------- /src/lib/server/storage.ts: -------------------------------------------------------------------------------- 1 | import { R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ACCOUNT_ID } from '$env/static/private'; 2 | import { S3Client } from '@aws-sdk/client-s3'; 3 | 4 | const r2Endpoint = `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`; 5 | 6 | export const s3 = new S3Client({ 7 | endpoint: r2Endpoint, 8 | credentials: { 9 | accessKeyId: R2_ACCESS_KEY_ID, 10 | secretAccessKey: R2_SECRET_ACCESS_KEY 11 | }, 12 | region: 'auto' 13 | }); 14 | -------------------------------------------------------------------------------- /src/lib/utils/helpers/generate.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase64url } from '@oslojs/encoding'; 2 | import { customAlphabet } from 'nanoid'; 3 | 4 | const ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; 5 | const ID_LENGTH = 12; 6 | 7 | export function generateNanoId() { 8 | const nanoid = customAlphabet(ID_ALPHABET, ID_LENGTH); 9 | return nanoid(); 10 | } 11 | 12 | export function generateToken() { 13 | const bytes = crypto.getRandomValues(new Uint8Array(64)); 14 | const token = encodeBase64url(bytes); 15 | return token; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/helpers/password.ts: -------------------------------------------------------------------------------- 1 | import { hash, verify } from '@node-rs/argon2'; 2 | 3 | export async function hashPassword(password: string) { 4 | const hashedPassword = await hash(password, { 5 | memoryCost: 19456, 6 | timeCost: 2, 7 | outputLen: 32, 8 | parallelism: 1 9 | }); 10 | return hashedPassword; 11 | } 12 | 13 | export async function verifyPassword(hashedPassword: string, password: string) { 14 | const isValid = await verify(hashedPassword, password, { 15 | memoryCost: 19456, 16 | timeCost: 2, 17 | outputLen: 32, 18 | parallelism: 1 19 | }); 20 | return isValid; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/utils/mail/templates/Welcome.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 18 | 19 |
    20 | 21 | 22 |

    Welcome to SvelteKit Omakase

    23 |
    24 | 25 |

    Hey, {userFirstName}!

    26 |

    We're thrilled to have you onboard. ⭐

    27 |
    28 | 29 | 30 |

    31 | Need help? we are just an email away. 32 |

    33 |
    34 |
    35 |
    36 | 37 | 38 | -------------------------------------------------------------------------------- /src/lib/utils/mail/templates/mailTheme.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/utils/mail/types.ts: -------------------------------------------------------------------------------- 1 | export interface EmailTemplate { 2 | subject: string; 3 | html: string; 4 | text: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/utils/themes.ts: -------------------------------------------------------------------------------- 1 | export const themes = { default: 'dark', light: 'light', dark: 'dark' }; 2 | -------------------------------------------------------------------------------- /src/lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export type WithoutChild = T extends { child?: any } ? Omit : T; 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | export type WithoutChildren = T extends { children?: any } ? Omit : T; 14 | export type WithoutChildrenOrChild = WithoutChildren>; 15 | export type WithElementRef = T & { ref?: U | null }; 16 | -------------------------------------------------------------------------------- /src/routes/(auth)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 |
    18 | 19 | {@render children?.()} 20 | 21 |
    22 | -------------------------------------------------------------------------------- /src/routes/(auth)/login/google/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestEvent } from '@sveltejs/kit'; 2 | import { generateState, generateCodeVerifier } from 'arctic'; 3 | import { google } from '$lib/server/oauth'; 4 | 5 | export async function GET(event: RequestEvent): Promise { 6 | const state = generateState(); 7 | const codeVerifier = generateCodeVerifier(); 8 | const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile', 'email']); 9 | 10 | event.cookies.set('google_oauth_state', state, { 11 | path: '/', 12 | httpOnly: true, 13 | maxAge: 60 * 10, // 10 minutes 14 | sameSite: 'lax' 15 | }); 16 | event.cookies.set('google_code_verifier', codeVerifier, { 17 | path: '/', 18 | httpOnly: true, 19 | maxAge: 60 * 10, // 10 minutes 20 | sameSite: 'lax' 21 | }); 22 | 23 | return new Response(null, { 24 | status: 302, 25 | headers: { 26 | Location: url.toString() 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/routes/(auth)/logout/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit'; 2 | import { redirect } from 'sveltekit-flash-message/server'; 3 | import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth'; 4 | import * as m from '$lib/utils/messages.json'; 5 | 6 | export const POST: RequestHandler = async (event) => { 7 | if (!event.locals.session) redirect(302, '/'); 8 | 9 | try { 10 | await invalidateSession(event.locals.session.id); 11 | deleteSessionTokenCookie(event); 12 | } catch (error) { 13 | console.log(error); 14 | redirect( 15 | '/', 16 | { 17 | status: 500, 18 | type: 'error', 19 | message: m.general.error 20 | }, 21 | event 22 | ); 23 | } 24 | 25 | redirect( 26 | '/', 27 | { 28 | type: 'success', 29 | message: m.auth.logout.success 30 | }, 31 | event 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/routes/(dashboard)/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { requireLogin } from '$lib/server/auth'; 2 | 3 | export async function load() { 4 | requireLogin(); 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/(dashboard)/dashboard/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | {#each Array(3)} 7 |
    8 | 9 |
    10 | {/each} 11 | 14 |
    15 | -------------------------------------------------------------------------------- /src/routes/(dashboard)/settings/+layout.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
    25 |
    26 |

    Settings

    27 |

    Manage your profile and teams.

    28 |
    29 | 30 |
    31 | 34 | 35 | 36 | 37 |
    38 |
    39 | {@render children?.()} 40 |
    41 |
    42 |
    43 |
    44 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 |

    8 | {page.status}: {page.error?.message} 9 |

    10 |

    11 | Go back home 12 |

    13 |
    14 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { loadFlash } from 'sveltekit-flash-message/server'; 2 | 3 | export const load = loadFlash(async (event) => { 4 | return { 5 | user: event.locals.user ?? null 6 | }; 7 | }); 8 | -------------------------------------------------------------------------------- /src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | 3 | export async function load({ locals }) { 4 | // redirect to `/dashboard` if logged in 5 | if (locals.user) redirect(302, '/dashboard'); 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/api/upload/+server.ts: -------------------------------------------------------------------------------- 1 | // Env Variables 2 | import { PUBLIC_R2_BUCKET_NAME } from '$env/static/public'; 3 | 4 | // Types 5 | import { type RequestHandler } from '@sveltejs/kit'; 6 | 7 | // Utils 8 | import { error, json } from '@sveltejs/kit'; 9 | import { s3 } from '$lib/server/storage'; 10 | import { PutObjectCommand } from '@aws-sdk/client-s3'; 11 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 12 | import crypto from 'crypto'; 13 | 14 | export const POST: RequestHandler = async ({ locals, request }) => { 15 | if (!locals.user) { 16 | error(401, 'Unauthorized'); 17 | } 18 | 19 | try { 20 | const { fileType, destinationDirectory } = await request.json(); 21 | 22 | const fileName = crypto.randomBytes(16).toString('hex'); 23 | 24 | const file = { 25 | Bucket: PUBLIC_R2_BUCKET_NAME, 26 | Key: `${destinationDirectory}/${fileName}`, 27 | ContentType: fileType 28 | }; 29 | 30 | const command = new PutObjectCommand(file); 31 | const url = await getSignedUrl(s3, command, { expiresIn: 60000 }); 32 | 33 | return json({ 34 | presignedUrl: url, 35 | fileName 36 | }); 37 | } catch (err) { 38 | console.log(err); 39 | } 40 | 41 | error(500, 'Something went wrong'); 42 | }; 43 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/8de5357d79ffdc52acae042c0731f3b6fda97135/static/favicon.png -------------------------------------------------------------------------------- /static/fonts/Montserrat-VariableFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/8de5357d79ffdc52acae042c0731f3b6fda97135/static/fonts/Montserrat-VariableFont.ttf -------------------------------------------------------------------------------- /static/fonts/Sora-VariableFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n00ki/sveltekit-omakase/8de5357d79ffdc52acae042c0731f3b6fda97135/static/fonts/Sora-VariableFont.ttf -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /admin -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/unit/demo.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /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 | } 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { svelteTesting } from '@testing-library/svelte/vite'; 4 | import tailwindcss from '@tailwindcss/vite'; 5 | 6 | export default defineConfig({ 7 | plugins: [sveltekit(), tailwindcss()], 8 | test: { 9 | workspace: [ 10 | { 11 | extends: './vite.config.ts', 12 | plugins: [svelteTesting()], 13 | test: { 14 | name: 'client', 15 | environment: 'jsdom', 16 | clearMocks: true, 17 | include: ['tests/**/*.svelte.{test,spec}.{js,ts}'], 18 | exclude: ['src/lib/server/**'], 19 | setupFiles: ['./vitest-setup-client.ts'] 20 | } 21 | }, 22 | { 23 | extends: './vite.config.ts', 24 | test: { 25 | name: 'server', 26 | environment: 'node', 27 | include: ['tests/unit/**/*.{test,spec}.{js,ts}'], 28 | exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] 29 | } 30 | } 31 | ] 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /vitest-setup-client.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { vi } from 'vitest'; 3 | 4 | // required for svelte5 + jsdom as jsdom does not support matchMedia 5 | Object.defineProperty(window, 'matchMedia', { 6 | writable: true, 7 | enumerable: true, 8 | value: vi.fn().mockImplementation((query) => ({ 9 | matches: false, 10 | media: query, 11 | onchange: null, 12 | addEventListener: vi.fn(), 13 | removeEventListener: vi.fn(), 14 | dispatchEvent: vi.fn() 15 | })) 16 | }); 17 | 18 | // more mocks here if needed 19 | --------------------------------------------------------------------------------