├── .editorconfig ├── .env.example ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .markuplintrc.cjs ├── .npmrc ├── .prettierignore ├── .prettierignore.root ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── apps ├── backend │ ├── #vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── .gitignore │ ├── README.md │ ├── package.json │ └── supabase │ │ ├── .gitignore │ │ ├── config.toml │ │ ├── migrations │ │ ├── 20240617090741_remote_schema.sql │ │ └── 20240617211147_remote_schema.sql │ │ └── seed.sql ├── mockup │ ├── .gitignore │ ├── README.md │ ├── app.css │ ├── commands │ │ ├── add-size-to-img.js │ │ ├── clean-image.sh │ │ ├── deploy.sh │ │ └── utils.js │ ├── eslint.config.js │ ├── package.json │ ├── public │ │ ├── apple-touch-icon.png │ │ ├── favicon.ico │ │ ├── images │ │ │ └── ogp.png │ │ ├── index.html │ │ └── script.js │ └── tests │ │ ├── add-size-to-img │ │ ├── add-size-to-img.test.js │ │ ├── dirs │ │ │ ├── dir0 │ │ │ │ └── dir00 │ │ │ │ │ ├── 00.html │ │ │ │ │ └── 00.js │ │ │ └── dir1 │ │ │ │ ├── 1.html │ │ │ │ └── dir10 │ │ │ │ └── 10.js │ │ └── html │ │ │ ├── expected.txt │ │ │ ├── images │ │ │ └── sample.svg │ │ │ └── input.txt │ │ ├── external-links.txt │ │ └── path.test.js └── web │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── app.css │ ├── app.html │ ├── index.test.ts │ ├── lib │ │ ├── $generated │ │ │ └── supabase-types.ts │ │ ├── components │ │ │ ├── Input.svelte │ │ │ ├── Meta.svelte │ │ │ ├── TextArea.svelte │ │ │ └── icons │ │ │ │ ├── 16x16 │ │ │ │ ├── PaperPlaneIcon.svelte │ │ │ │ ├── SignInIcon.svelte │ │ │ │ └── SignOutIcon.svelte │ │ │ │ └── 20x20 │ │ │ │ ├── CircleCheckIcon.svelte │ │ │ │ └── CircleCloseIcon.svelte │ │ ├── easing.ts │ │ ├── features │ │ │ ├── comment │ │ │ │ ├── Comment.svelte │ │ │ │ ├── Comments.svelte │ │ │ │ ├── commentRequests.ts │ │ │ │ └── commentStore.svelte.ts │ │ │ └── user │ │ │ │ ├── OnAuthStateChange.svelte │ │ │ │ ├── userRequests.ts │ │ │ │ └── userStore.svelte.ts │ │ ├── routes.ts │ │ ├── supabase.ts │ │ └── variants │ │ │ ├── buttonVariants.ts │ │ │ └── sectionFrameVariants.ts │ └── routes │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ ├── CommentForm.svelte │ │ ├── Footer.svelte │ │ ├── GA4.svelte │ │ ├── HeaderNavigation.svelte │ │ ├── HeaderNavigationItems.svelte │ │ ├── LoginMessage.svelte │ │ ├── admin │ │ ├── (isNotLoggedIn) │ │ │ ├── +layout.svelte │ │ │ ├── UserInputs.svelte │ │ │ ├── login │ │ │ │ └── +page.svelte │ │ │ └── signup │ │ │ │ └── +page.svelte │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ ├── AdminHeaderMessage.svelte │ │ ├── AdminHeaderTabs.svelte │ │ └── LoggedInMessage.svelte │ │ └── secret │ │ └── +page.svelte │ ├── static │ ├── apple-touch-icon.png │ ├── favicon.ico │ └── images │ │ └── ogp.png │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── commands ├── format-project-words.sh ├── init.sh └── use-mockup.js ├── cspell.json ├── eslint.config.js ├── package.json ├── packages └── eslint-config │ ├── eslint.config.js │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js ├── project-words.txt └── turbo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.{txt,tsv,csv}] 17 | indent_style = tab 18 | insert_final_newline = false 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # SvelteKit (`apps/web`) 2 | 3 | PUBLIC_GA4_MEASUREMENT_ID= 4 | PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 5 | PUBLIC_SUPABASE_ANON_KEY= 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 23 | jobs: 24 | # This workflow contains a single job called "build" 25 | build: 26 | strategy: 27 | matrix: 28 | os: [ubuntu-latest] 29 | node: [20] 30 | 31 | name: Build (Node ${{ matrix.node }} on ${{ matrix.os }}) 32 | 33 | # The type of runner that the job will run on 34 | runs-on: ${{ matrix.os }} 35 | timeout-minutes: 8 36 | 37 | # Steps represent a sequence of tasks that will be executed as part of the job 38 | steps: 39 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 40 | - name: Checkout 🛎 41 | uses: actions/checkout@v4 42 | 43 | - name: Setup pnpm 📦 44 | uses: pnpm/action-setup@v4 45 | 46 | - name: Setup node 🏗 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ matrix.node }} 50 | cache: 'pnpm' 51 | 52 | - name: Install dependencies 👨🏻‍💻 53 | run: pnpm install --frozen-lockfile 54 | 55 | - name: Run build 🐣 56 | run: pnpm build 57 | env: 58 | PUBLIC_GA4_MEASUREMENT_ID: ${{ secrets.PUBLIC_GA4_MEASUREMENT_ID }} 59 | PUBLIC_SUPABASE_URL: ${{ secrets.PUBLIC_SUPABASE_URL }} 60 | PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.PUBLIC_SUPABASE_ANON_KEY }} 61 | 62 | - name: Run lint 👀 63 | run: pnpm lint 64 | 65 | - name: Run test 🧪 66 | run: pnpm test 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # svelte 12 | .svelte-kit 13 | 14 | # misc 15 | .DS_Store 16 | *.pem 17 | 18 | # debug 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # turbo 24 | .turbo 25 | 26 | # custom 27 | .env 28 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | pnpm test 3 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '*': ['cspell --no-must-find-files', 'prettier --write --ignore-unknown'], 3 | '*.html': ['markuplint'], 4 | '*.{js,cjs,mjs,jsx,ts,tsx}': ['eslint --fix'], 5 | '*.svelte': ['markuplint', 'eslint --fix'], 6 | }; 7 | -------------------------------------------------------------------------------- /.markuplintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: { 3 | '\\.svelte$': '@markuplint/svelte-parser', 4 | }, 5 | extends: ['markuplint:recommended'], 6 | excludeFiles: [ 7 | // TODO: Once the overrides option is fixed, remove these lines 8 | // ref. https://github.com/markuplint/markuplint/issues/1119 9 | './apps/web/src/app.html', 10 | // TODO: for Svelte 5 (preview) 11 | './apps/web/**/*.svelte', 12 | ], 13 | rules: { 14 | 'character-reference': false, 15 | 'ineffective-attr': false, 16 | 'label-has-control': false, 17 | 'require-accessible-name': false, 18 | }, 19 | nodeRules: [ 20 | // For Svelte 21 | { 22 | selector: 'textarea', 23 | rules: { 24 | 'invalid-attr': { 25 | options: { 26 | allowAttrs: ['value'], 27 | }, 28 | }, 29 | }, 30 | }, 31 | { 32 | selector: 'input[type="file"]', 33 | rules: { 34 | 'invalid-attr': { 35 | options: { 36 | allowAttrs: ['files'], 37 | }, 38 | }, 39 | }, 40 | }, 41 | ], 42 | }; 43 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-manager-strict=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .env 4 | .env.* 5 | !.env.example 6 | 7 | # Ignore files for PNPM 8 | pnpm-lock.yaml 9 | pnpm-workspace.yaml 10 | 11 | # custom 12 | .turbo 13 | *.min.* 14 | /apps/backend/$generated 15 | /apps/mockup/public/styles.css 16 | /apps/web/.svelte-kit 17 | /apps/web/src/lib/$generated 18 | -------------------------------------------------------------------------------- /.prettierignore.root: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .env 4 | .env.* 5 | !.env.example 6 | 7 | # Ignore files for PNPM 8 | pnpm-lock.yaml 9 | pnpm-workspace.yaml 10 | 11 | # custom 12 | .turbo 13 | /apps/ 14 | /packages/ 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "dbaeumer.vscode-eslint", 5 | "EditorConfig.EditorConfig", 6 | "esbenp.prettier-vscode", 7 | "svelte.svelte-vscode" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "prettier.requireConfig": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.quickSuggestions": { 6 | "strings": true 7 | }, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | }, 11 | "eslint.workingDirectories": [{ "mode": "auto" }], 12 | "eslint.useFlatConfig": true, 13 | "eslint.validate": ["svelte"], 14 | "svelte.enable-ts-plugin": true, 15 | "tailwindCSS.experimental.classRegex": [ 16 | ["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 usagizmo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebApp Template 2 | 3 | Monorepo template for creating a web application. 4 | 5 | ## What's inside? 6 | 7 | ### Uses 8 | 9 | - [Turborepo](https://turborepo.org/) x [pnpm](https://pnpm.io/) 10 | - [Prettier](https://prettier.io/) (w/ [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte) + [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)) 11 | - [ESLint](https://eslint.org/) / [CSpell](https://cspell.org/) 12 | - [lint-staged](https://github.com/okonet/lint-staged) / [husky](https://github.com/typicode/husky) 13 | - GitHub Actions (Linting + Testing (Validate `href` and `src` paths)) 14 | - Execute `eslint --fix` and `prettier` when saving with VS Code 15 | 16 | ### Apps and Packages 17 | 18 | #### `apps/` 19 | 20 | - [`backend`](./apps/backend/) 21 | A [Supabase](https://supabase.io/) [Local Dev / CLI](https://supabase.com/docs/guides/cli). 22 | - [`mockup`](./apps/mockup/) [[Demo](https://webapp-template-mockup.usagizmo.com/)] 23 | A starting point for building a static site. 24 | [Tailwind CSS](https://tailwindcss.com/) + Vanilla JS + [Vitest](https://vitest.dev/) (Check links + file names) 25 | - [`web`](./apps/web/) [[Demo](https://webapp-template.usagizmo.com/)] 26 | A starting point for building Svelte application. 27 | [SvelteKit](https://svelte.dev/docs/kit/) (w/ [Tailwind CSS](https://tailwindcss.com/)) 28 | [Supabase](https://supabase.io/) / [Vitest](https://vitest.dev/) 29 | 30 | #### `packages/` 31 | 32 | - `eslint-config` 33 | ESLint 9 (Flat Config) for JavaScript and TypeScript. 34 | - [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) 35 | - [eslint-plugin-svelte](https://github.com/sveltejs/eslint-plugin-svelte) 36 | - [eslint-plugin-simple-import-sort](https://github.com/lydell/eslint-plugin-simple-import-sort) 37 | - [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) 38 | 39 | ### VS Code Extensions (Recommend) 40 | 41 | - [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 42 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 43 | - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 44 | - [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) 45 | - [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 46 | 47 | ## Commands 48 | 49 | ```bash 50 | pnpm i # Resolve dependency packages and prepare .env files 51 | # Then set up /.env 52 | 53 | # Run command for each package (apps/ + packages/) 54 | pnpm generate # Generate and sync Supabase type definitions between backend and web apps 55 | pnpm build # 56 | pnpm lint # root: cspell + prettier --check 57 | pnpm test # 58 | pnpm format # root: Format project-words.txt + prettier --write 59 | 60 | # Optional 61 | pnpm use-mockup # For mockup-only usage: Removes unnecessary files/lines. 62 | ``` 63 | 64 | ### Supabase Type Generation 65 | 66 | Run `pnpm generate` to generate Supabase types. This command will: 67 | 68 | 1. Generate types in `apps/backend/$generated/supabase-types.ts` 69 | 2. Copy the types to `apps/web/src/lib/$generated/supabase-types.ts` 70 | 71 | This ensures type consistency between the backend and frontend applications. 72 | 73 | ## List of listening port numbers 74 | 75 | - `apps/backend/` - Supabase Local Dev / CLI 76 | - `54321`: API / GraphQL / S3 Storage 77 | - `54322`: DB (Postgres) 78 | - `54323`: Studio 79 | - `54324`: Inbucket 80 | - `apps/web/` - SvelteKit application 81 | - `5173`: Development server 82 | - `apps/mockup/` - Static site 83 | - `3000`: BrowserSync server 84 | - `49160`: Express server 85 | 86 | ## Registering environment variables for GitHub / Vercel 87 | 88 | If you need to prepare GitHub / Vercel environment, you need to set all environment variables (`.env` items) in each service. 89 | 90 | ## Breaking changes 91 | 92 | ### v2.0.0 93 | 94 | - **Update Framework/Library Versions:** 95 | - Switch to Svelte 5 (integrated with TypeScript and using the Rune) 96 | - Update to Tailwind CSS 4 (removed `tailwind.config.js`) 97 | - Upgrade to ESLint 9 and implement Flat Config 98 | - **Backend Change:** 99 | - Replace [Nhost](https://nhost.io/) with [Supabase](https://supabase.com/) for backend services 100 | 101 | ### v1.9.0 102 | 103 | - **Language and Compiler Changes:** 104 | - Migrated codebase from JavaScript to TypeScript 105 | - Upgraded from Svelte 4 to Svelte 5 (Rune) 106 | - **Package Naming and Structure:** 107 | - Custom package names now prefixed with `@repo/` 108 | - Merged `eslint-config-custom-typescript` into `eslint-config-custom` 109 | 110 | ### v1.6.0 111 | 112 | - **Language Reversion and Documentation:** 113 | - Reverted codebase from TypeScript back to JavaScript, supplementing with JSDoc for documentation 114 | 115 | ### v1.0.0 116 | 117 | - **Frontend Framework Change:** 118 | - Switched from [Next.js](https://nextjs.org/) to [SvelteKit](https://svelte.dev/docs/kit/) for the frontend framework in `apps/web` 119 | - **Repository Rebranding:** 120 | - Renamed `nextjs-template` repository to `webapp-template` 121 | 122 | ### v0.23.0 123 | 124 | - **Backend Services Integration:** 125 | - Replaced individual [Firebase](https://firebase.google.com/) and [Hasura](https://hasura.io/) applications with a unified [Nhost](https://nhost.io/) application in `apps/nhost` 126 | -------------------------------------------------------------------------------- /apps/backend/#vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/backend/#vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enablePaths": ["supabase/functions"], 3 | "deno.lint": true, 4 | "deno.unstable": true, 5 | "[typescript]": { 6 | "editor.defaultFormatter": "denoland.vscode-deno" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/backend/.gitignore: -------------------------------------------------------------------------------- 1 | $generated/* 2 | -------------------------------------------------------------------------------- /apps/backend/README.md: -------------------------------------------------------------------------------- 1 | # `backend` app 2 | 3 | This app is a [Supabase](https://supabase.io/) [Local Dev / CLI](https://supabase.com/docs/guides/cli). 4 | 5 | ## Commands 6 | 7 | ```bash 8 | pnpm pull # Pull the latest changes from the supabase server 9 | pnpm generate # Generate Supabase types to $generated/supabase-types.ts 10 | pnpm start # Start the supabase server 11 | pnpm stop # Stop the supabase server 12 | ``` 13 | -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "pull": "supabase db pull --local --schema public,auth,storage", 8 | "generate": "mkdir -p \\$generated && supabase gen types typescript --local > ./\\$generated/supabase-types.ts", 9 | "start": "supabase start", 10 | "stop": "supabase stop" 11 | }, 12 | "devDependencies": { 13 | "supabase": "^2.0.6" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/backend/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /apps/backend/supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the 2 | # working directory name when running `supabase init`. 3 | project_id = "webapp-template-backend" 4 | 5 | [api] 6 | enabled = true 7 | # Port to use for the API URL. 8 | port = 54321 9 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 10 | # endpoints. `public` is always included. 11 | schemas = ["public", "graphql_public"] 12 | # Extra schemas to add to the search_path of every request. `public` is always included. 13 | extra_search_path = ["public", "extensions"] 14 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 15 | # for accidental or malicious requests. 16 | max_rows = 1000 17 | 18 | [db] 19 | # Port to use for the local database URL. 20 | port = 54322 21 | # Port used by db diff command to initialize the shadow database. 22 | shadow_port = 54320 23 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 24 | # server_version;` on the remote database to check. 25 | major_version = 15 26 | 27 | [db.pooler] 28 | enabled = false 29 | # Port to use for the local connection pooler. 30 | port = 54329 31 | # Specifies when a server connection can be reused by other clients. 32 | # Configure one of the supported pooler modes: `transaction`, `session`. 33 | pool_mode = "transaction" 34 | # How many server connections to allow per user/database pair. 35 | default_pool_size = 20 36 | # Maximum number of client connections allowed. 37 | max_client_conn = 100 38 | 39 | [realtime] 40 | enabled = true 41 | # Bind realtime via either IPv4 or IPv6. (default: IPv4) 42 | # ip_version = "IPv6" 43 | # The maximum length in bytes of HTTP request headers. (default: 4096) 44 | # max_header_length = 4096 45 | 46 | [studio] 47 | enabled = true 48 | # Port to use for Supabase Studio. 49 | port = 54323 50 | # External URL of the API server that frontend connects to. 51 | api_url = "http://127.0.0.1" 52 | # OpenAI API Key to use for Supabase AI in the Supabase Studio. 53 | openai_api_key = "env(OPENAI_API_KEY)" 54 | 55 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 56 | # are monitored, and you can view the emails that would have been sent from the web interface. 57 | [inbucket] 58 | enabled = true 59 | # Port to use for the email testing server web interface. 60 | port = 54324 61 | # Uncomment to expose additional ports for testing user applications that send emails. 62 | # smtp_port = 54325 63 | # pop3_port = 54326 64 | 65 | [storage] 66 | enabled = true 67 | # The maximum file size allowed (e.g. "5MB", "500KB"). 68 | file_size_limit = "50MiB" 69 | 70 | [storage.image_transformation] 71 | enabled = true 72 | 73 | [auth] 74 | enabled = true 75 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 76 | # in emails. 77 | site_url = "http://127.0.0.1:3000" 78 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 79 | additional_redirect_urls = ["https://127.0.0.1:3000"] 80 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). 81 | jwt_expiry = 3600 82 | # If disabled, the refresh token will never expire. 83 | enable_refresh_token_rotation = true 84 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. 85 | # Requires enable_refresh_token_rotation = true. 86 | refresh_token_reuse_interval = 10 87 | # Allow/disallow new user signups to your project. 88 | enable_signup = true 89 | # Allow/disallow anonymous sign-ins to your project. 90 | enable_anonymous_sign_ins = false 91 | # Allow/disallow testing manual linking of accounts 92 | enable_manual_linking = false 93 | 94 | [auth.email] 95 | # Allow/disallow new user signups via email to your project. 96 | enable_signup = true 97 | # If enabled, a user will be required to confirm any email change on both the old, and new email 98 | # addresses. If disabled, only the new email is required to confirm. 99 | double_confirm_changes = true 100 | # If enabled, users need to confirm their email address before signing in. 101 | enable_confirmations = false 102 | # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. 103 | max_frequency = "1s" 104 | 105 | # Uncomment to customize email template 106 | # [auth.email.template.invite] 107 | # subject = "You have been invited" 108 | # content_path = "./supabase/templates/invite.html" 109 | 110 | [auth.sms] 111 | # Allow/disallow new user signups via SMS to your project. 112 | enable_signup = true 113 | # If enabled, users need to confirm their phone number before signing in. 114 | enable_confirmations = false 115 | # Template for sending OTP to users 116 | template = "Your code is {{ .Code }} ." 117 | # Controls the minimum amount of time that must pass before sending another sms otp. 118 | max_frequency = "5s" 119 | 120 | # Use pre-defined map of phone number to OTP for testing. 121 | # [auth.sms.test_otp] 122 | # 4152127777 = "123456" 123 | 124 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. 125 | # [auth.hook.custom_access_token] 126 | # enabled = true 127 | # uri = "pg-functions:////" 128 | 129 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. 130 | [auth.sms.twilio] 131 | enabled = false 132 | account_sid = "" 133 | message_service_sid = "" 134 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: 135 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" 136 | 137 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 138 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, 139 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 140 | [auth.external.apple] 141 | enabled = false 142 | client_id = "" 143 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: 144 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" 145 | # Overrides the default auth redirectUrl. 146 | redirect_uri = "" 147 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 148 | # or any other third-party OIDC providers. 149 | url = "" 150 | # If enabled, the nonce check will be skipped. Required for local sign in with Google auth. 151 | skip_nonce_check = false 152 | 153 | [analytics] 154 | enabled = false 155 | port = 54327 156 | vector_port = 54328 157 | # Configure one of the supported backends: `postgres`, `bigquery`. 158 | backend = "postgres" 159 | 160 | # Experimental features may be deprecated any time 161 | [experimental] 162 | # Configures Postgres storage engine to use OrioleDB (S3) 163 | orioledb_version = "" 164 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com 165 | s3_host = "env(S3_HOST)" 166 | # Configures S3 bucket region, eg. us-east-1 167 | s3_region = "env(S3_REGION)" 168 | # Configures AWS_ACCESS_KEY_ID for S3 bucket 169 | s3_access_key = "env(S3_ACCESS_KEY)" 170 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket 171 | s3_secret_key = "env(S3_SECRET_KEY)" 172 | -------------------------------------------------------------------------------- /apps/backend/supabase/migrations/20240617090741_remote_schema.sql: -------------------------------------------------------------------------------- 1 | 2 | SET statement_timeout = 0; 3 | SET lock_timeout = 0; 4 | SET idle_in_transaction_session_timeout = 0; 5 | SET client_encoding = 'UTF8'; 6 | SET standard_conforming_strings = on; 7 | SELECT pg_catalog.set_config('search_path', '', false); 8 | SET check_function_bodies = false; 9 | SET xmloption = content; 10 | SET client_min_messages = warning; 11 | SET row_security = off; 12 | 13 | CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions"; 14 | 15 | CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium"; 16 | 17 | COMMENT ON SCHEMA "public" IS 'standard public schema'; 18 | 19 | CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; 20 | 21 | CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; 22 | 23 | CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; 24 | 25 | CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions"; 26 | 27 | CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; 28 | 29 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; 30 | 31 | ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres"; 32 | 33 | GRANT USAGE ON SCHEMA "public" TO "postgres"; 34 | GRANT USAGE ON SCHEMA "public" TO "anon"; 35 | GRANT USAGE ON SCHEMA "public" TO "authenticated"; 36 | GRANT USAGE ON SCHEMA "public" TO "service_role"; 37 | 38 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres"; 39 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon"; 40 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated"; 41 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role"; 42 | 43 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres"; 44 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon"; 45 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated"; 46 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role"; 47 | 48 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres"; 49 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon"; 50 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated"; 51 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role"; 52 | 53 | RESET ALL; 54 | -------------------------------------------------------------------------------- /apps/backend/supabase/migrations/20240617211147_remote_schema.sql: -------------------------------------------------------------------------------- 1 | create table "public"."comments" ( 2 | "id" bigint generated by default as identity not null, 3 | "created_at" timestamp with time zone not null default now(), 4 | "text" text not null default ''::text, 5 | "user_id" uuid not null, 6 | "file_path" text 7 | ); 8 | 9 | 10 | alter table "public"."comments" enable row level security; 11 | 12 | create table "public"."profiles" ( 13 | "id" uuid not null, 14 | "created_at" timestamp with time zone not null default now(), 15 | "bio" text not null default ''::text, 16 | "display_name" text not null default ''::text, 17 | "email" character varying not null 18 | ); 19 | 20 | 21 | alter table "public"."profiles" enable row level security; 22 | 23 | CREATE UNIQUE INDEX comments_pkey ON public.comments USING btree (id); 24 | 25 | CREATE UNIQUE INDEX profiles_email_key ON public.profiles USING btree (email); 26 | 27 | CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id); 28 | 29 | alter table "public"."comments" add constraint "comments_pkey" PRIMARY KEY using index "comments_pkey"; 30 | 31 | alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey"; 32 | 33 | alter table "public"."comments" add constraint "public_comments_user_id_fkey" FOREIGN KEY (user_id) REFERENCES profiles(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 34 | 35 | alter table "public"."comments" validate constraint "public_comments_user_id_fkey"; 36 | 37 | alter table "public"."profiles" add constraint "profiles_email_key" UNIQUE using index "profiles_email_key"; 38 | 39 | alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; 40 | 41 | alter table "public"."profiles" validate constraint "profiles_id_fkey"; 42 | 43 | alter table "public"."profiles" add constraint "public_profiles_user_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 44 | 45 | alter table "public"."profiles" validate constraint "public_profiles_user_id_fkey"; 46 | 47 | set check_function_bodies = off; 48 | 49 | CREATE OR REPLACE FUNCTION public.handle_new_user() 50 | RETURNS trigger 51 | LANGUAGE plpgsql 52 | SECURITY DEFINER 53 | SET search_path TO '' 54 | AS $function$ 55 | DECLARE 56 | display_name text; 57 | BEGIN 58 | IF new.raw_user_meta_data IS NOT NULL AND new.raw_user_meta_data ? 'display_name' THEN 59 | display_name := new.raw_user_meta_data ->> 'display_name'; 60 | ELSE 61 | display_name := ''; 62 | END IF; 63 | 64 | INSERT INTO public.profiles (id, email, display_name) 65 | VALUES (new.id, new.email, display_name); 66 | 67 | RETURN new; 68 | END; 69 | $function$ 70 | ; 71 | 72 | grant delete on table "public"."comments" to "anon"; 73 | 74 | grant insert on table "public"."comments" to "anon"; 75 | 76 | grant references on table "public"."comments" to "anon"; 77 | 78 | grant select on table "public"."comments" to "anon"; 79 | 80 | grant trigger on table "public"."comments" to "anon"; 81 | 82 | grant truncate on table "public"."comments" to "anon"; 83 | 84 | grant update on table "public"."comments" to "anon"; 85 | 86 | grant delete on table "public"."comments" to "authenticated"; 87 | 88 | grant insert on table "public"."comments" to "authenticated"; 89 | 90 | grant references on table "public"."comments" to "authenticated"; 91 | 92 | grant select on table "public"."comments" to "authenticated"; 93 | 94 | grant trigger on table "public"."comments" to "authenticated"; 95 | 96 | grant truncate on table "public"."comments" to "authenticated"; 97 | 98 | grant update on table "public"."comments" to "authenticated"; 99 | 100 | grant delete on table "public"."comments" to "service_role"; 101 | 102 | grant insert on table "public"."comments" to "service_role"; 103 | 104 | grant references on table "public"."comments" to "service_role"; 105 | 106 | grant select on table "public"."comments" to "service_role"; 107 | 108 | grant trigger on table "public"."comments" to "service_role"; 109 | 110 | grant truncate on table "public"."comments" to "service_role"; 111 | 112 | grant update on table "public"."comments" to "service_role"; 113 | 114 | grant delete on table "public"."profiles" to "anon"; 115 | 116 | grant insert on table "public"."profiles" to "anon"; 117 | 118 | grant references on table "public"."profiles" to "anon"; 119 | 120 | grant select on table "public"."profiles" to "anon"; 121 | 122 | grant trigger on table "public"."profiles" to "anon"; 123 | 124 | grant truncate on table "public"."profiles" to "anon"; 125 | 126 | grant update on table "public"."profiles" to "anon"; 127 | 128 | grant delete on table "public"."profiles" to "authenticated"; 129 | 130 | grant insert on table "public"."profiles" to "authenticated"; 131 | 132 | grant references on table "public"."profiles" to "authenticated"; 133 | 134 | grant select on table "public"."profiles" to "authenticated"; 135 | 136 | grant trigger on table "public"."profiles" to "authenticated"; 137 | 138 | grant truncate on table "public"."profiles" to "authenticated"; 139 | 140 | grant update on table "public"."profiles" to "authenticated"; 141 | 142 | grant delete on table "public"."profiles" to "service_role"; 143 | 144 | grant insert on table "public"."profiles" to "service_role"; 145 | 146 | grant references on table "public"."profiles" to "service_role"; 147 | 148 | grant select on table "public"."profiles" to "service_role"; 149 | 150 | grant trigger on table "public"."profiles" to "service_role"; 151 | 152 | grant truncate on table "public"."profiles" to "service_role"; 153 | 154 | grant update on table "public"."profiles" to "service_role"; 155 | 156 | create policy "Enable delete for users based on user_id" 157 | on "public"."comments" 158 | as permissive 159 | for delete 160 | to public 161 | using ((auth.uid() = user_id)); 162 | 163 | 164 | create policy "Enable insert for users based on user_id" 165 | on "public"."comments" 166 | as permissive 167 | for insert 168 | to public 169 | with check ((auth.uid() = user_id)); 170 | 171 | 172 | create policy "Enable read access for all users" 173 | on "public"."comments" 174 | as permissive 175 | for select 176 | to public 177 | using (true); 178 | 179 | 180 | create policy "Enable update for users based on user_id" 181 | on "public"."comments" 182 | as permissive 183 | for update 184 | to public 185 | using ((auth.uid() = user_id)) 186 | with check ((auth.uid() = user_id)); 187 | 188 | 189 | create policy "Enable read access for all users" 190 | on "public"."profiles" 191 | as permissive 192 | for select 193 | to public 194 | using (true); 195 | 196 | 197 | create policy "Enable update for users based on id" 198 | on "public"."profiles" 199 | as permissive 200 | for update 201 | to public 202 | using ((auth.uid() = id)) 203 | with check ((auth.uid() = id)); 204 | 205 | 206 | 207 | CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user(); 208 | 209 | 210 | create policy "Give all users access" 211 | on "storage"."objects" 212 | as permissive 213 | for select 214 | to public 215 | using ((bucket_id = 'comments'::text)); 216 | 217 | 218 | create policy "Give users access to own folder (DELETE)" 219 | on "storage"."objects" 220 | as permissive 221 | for delete 222 | to public 223 | using (((bucket_id = 'comments'::text) AND ((auth.uid())::text = (storage.foldername(name))[1]))); 224 | 225 | 226 | create policy "Give users access to own folder" 227 | on "storage"."objects" 228 | as permissive 229 | for insert 230 | to public 231 | with check (((bucket_id = 'comments'::text) AND ((auth.uid())::text = (storage.foldername(name))[1]))); 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /apps/backend/supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -- Seed the storage.buckets table with the default buckets 2 | insert into storage.buckets 3 | (id, name, public, avif_autodetection, file_size_limit) 4 | values 5 | ('comments', 'comments', true, false, 5242880); 6 | -------------------------------------------------------------------------------- /apps/mockup/.gitignore: -------------------------------------------------------------------------------- 1 | /public/styles.css 2 | -------------------------------------------------------------------------------- /apps/mockup/README.md: -------------------------------------------------------------------------------- 1 | # `mockup` app 2 | 3 | A starting point for building a static site. 4 | 5 | [[Demo](https://webapp-template-mockup.usagizmo.com/)] 6 | 7 | ## Commands 8 | 9 | ```bash 10 | pnpm build # Output `public/styles.css` 11 | pnpm dev # Watch app.css and launch browser-sync server on port 3000 12 | pnpm lint # markuplint + cspell 13 | pnpm test # Check links (href/src) + image file names 14 | pnpm format # Format with `prettier` 15 | 16 | # `commands/*` 17 | pnpm add-size-to-img # Add width, height attributes to based on actual image size 18 | pnpm clean-image # Remove unused image files in `public/images/*` 19 | pnpm deploy # When deploying to a VPS such as DigitalOcean using `rsync` 20 | ``` 21 | 22 | ## tests/external-links.txt 23 | 24 | This is a list of external URLs or non-existent file paths specified by links (`href/src`) in HTML files. 25 | If this file does not exist, it is output by `pnpm test`. 26 | If present, test for any changes to the content. 27 | 28 | ## Deploy to Vercel (apps/mockup) 29 | 30 | - Framework Preset: `Other` 31 | - Root Directory: `apps/mockup` 32 | - Build Command: `cd ../.. && npx turbo run build --filter=mockup` 33 | - Corepack Configuration: Add the following environment variable to enable pnpm@10: 34 | - Key: `ENABLE_EXPERIMENTAL_COREPACK` 35 | - Value: `1` 36 | 37 | ### With Basic Authentication 38 | 39 | ```bash 40 | # Add packages 41 | pnpm add -D express express-basic-auth cors 42 | ``` 43 | 44 | Run the following, then change the `username` and `password` in `index.cjs`. 45 | 46 | ```bash 47 | # vercel.json 48 | printf "{ 49 | \"builds\": [ 50 | { 51 | \"src\": \"index.cjs\", 52 | \"use\": \"@vercel/node\" 53 | } 54 | ], 55 | \"routes\": [{ \"src\": \"/.*\", \"dest\": \"index.cjs\" }] 56 | } 57 | " > vercel.json 58 | 59 | # index.cjs 60 | printf "const path = require('path'); 61 | const cors = require('cors'); 62 | const express = require('express'); 63 | const basicAuth = require('express-basic-auth'); 64 | const app = express(); 65 | 66 | // Local runtime port number 67 | // Any number will be ignored by Vercel and will work 68 | const port = 49160; 69 | 70 | app.use(cors()); 71 | 72 | app.use( 73 | basicAuth({ 74 | users: { 75 | : '', 76 | }, 77 | challenge: true, 78 | }) 79 | ); 80 | 81 | app.use(express.static(path.join(__dirname, '/public'))); 82 | 83 | app.listen(port, () => { 84 | console.log(\`Listening on http://localhost:\${port}\`); 85 | }); 86 | 87 | module.exports = app; 88 | " > index.cjs 89 | ``` 90 | 91 | You can verify basic authentication by running `node index.cjs`. 92 | -------------------------------------------------------------------------------- /apps/mockup/app.css: -------------------------------------------------------------------------------- 1 | /* ref. https://tailwindcss.com/docs/theme */ 2 | @import 'tailwindcss'; 3 | 4 | @theme { 5 | /* --font-sans: -apple-system, blinkMacSystemFont, Helvetica, 'Yu Gothic', YuGothic, 6 | 'BIZ UDPGothic', Meiryo, sans-serif; 7 | --font-sans: -apple-system, blinkMacSystemFont, Helvetica, 'Hiragino Sans', 8 | 'Hiragino Kaku Gothic ProN', 'BIZ UDPGothic', Meiryo, sans-serif; 9 | --font-serif: 'Yu Mincho', YuMincho, 'Hiragino Mincho ProN', serif; */ 10 | --font-sans: YakuHanJP, 'Noto Sans JP', sans-serif; 11 | --font-ui: Inter, YakuHanJP, 'Noto Sans JP', sans-serif; 12 | --font-mono: 'JetBrains Mono', monospace; 13 | } 14 | -------------------------------------------------------------------------------- /apps/mockup/commands/add-size-to-img.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises'; 2 | 3 | import { dirname, join } from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | import { convert, deepReaddir } from './utils.js'; 7 | 8 | const PUBLIC_DIR = 'public'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | const rootDir = join(__dirname, '..'); 14 | 15 | /** 16 | * Process all the html files in the public directory 17 | * @returns {Promise} 18 | */ 19 | async function processHtmlFiles() { 20 | const htmlPaths = await deepReaddir(join(rootDir, PUBLIC_DIR), { ext: '.html' }); 21 | 22 | await Promise.all( 23 | htmlPaths.map(async (htmlPath) => { 24 | // convert the width and height of the images in the html file 25 | const nextHtml = await convert(htmlPath); 26 | 27 | try { 28 | await writeFile(htmlPath, nextHtml, 'utf8'); 29 | } catch (err) { 30 | console.error(err); 31 | } 32 | }), 33 | ); 34 | } 35 | 36 | processHtmlFiles(); 37 | -------------------------------------------------------------------------------- /apps/mockup/commands/clean-image.sh: -------------------------------------------------------------------------------- 1 | for FILE in $(git ls-files ./public/images); do 2 | git grep $(basename "$FILE") > /dev/null || git rm "$FILE" 3 | done 4 | -------------------------------------------------------------------------------- /apps/mockup/commands/deploy.sh: -------------------------------------------------------------------------------- 1 | rsync -ahvu --delete --exclude=".*" public/ :/var/www/html/ 2 | echo "\n🚀 \x1b[32mhttps://webapp-template-mockup.usagizmo.com/\x1b[0m\n" 3 | -------------------------------------------------------------------------------- /apps/mockup/commands/utils.js: -------------------------------------------------------------------------------- 1 | import { readdir, readFile } from 'node:fs/promises'; 2 | 3 | import sizeOf from 'image-size'; 4 | import { dirname, join } from 'path'; 5 | 6 | /** 7 | * Recursively read a directory and return all the paths of the files that match the extension 8 | * @param {string} dirPath - The path of the directory to read 9 | * @param {object} [options] - The options 10 | * @param {string} [options.ext] - The extension to match 11 | * @returns {Promise} - The paths of the files that match the extension 12 | */ 13 | export async function deepReaddir(dirPath, options) { 14 | const ext = options?.ext ?? ''; 15 | const dirents = await readdir(dirPath, { withFileTypes: true }); 16 | 17 | const filteredPath = (path) => (path.endsWith(ext) ? path : null); 18 | const paths = ( 19 | await Promise.all( 20 | dirents.map(async (dirent) => { 21 | const path = join(dirPath, dirent.name); 22 | return dirent.isDirectory() ? await deepReaddir(path, options) : filteredPath(path); 23 | }), 24 | ) 25 | ).filter(Boolean); 26 | 27 | return paths ? paths.flat() : []; 28 | } 29 | 30 | /** 31 | * Convert the width and height of the images in the html file 32 | * @param {string} filePath - The path of the html file 33 | * @returns {Promise} - The html file with the width and height of the images 34 | */ 35 | export async function convert(filePath) { 36 | const html = await readFile(filePath, 'utf8'); 37 | 38 | const res = html.replace( 39 | /(])+?src=")([^"]+?)("(?:(?!(?:width|height)=")[^>])+?>)/g, 40 | (_, prefix, imgSrcPath, suffix) => { 41 | const imagePath = join(dirname(filePath), imgSrcPath); 42 | 43 | try { 44 | const { width, height } = sizeOf(imagePath); 45 | return `${prefix}${imgSrcPath}" width="${width}" height="${height}${suffix}`; 46 | } catch { 47 | return prefix + imgSrcPath + suffix; 48 | } 49 | }, 50 | ); 51 | 52 | return res; 53 | } 54 | -------------------------------------------------------------------------------- /apps/mockup/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from '@repo/eslint-config'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /apps/mockup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockup", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "tailwindcss -i ./app.css -o ./public/styles.css --minify", 8 | "dev:tailwind": "tailwindcss -i ./app.css -o ./public/styles.css --watch", 9 | "dev:server": "browser-sync start --server ./public/ --files ./public/ --startPath /", 10 | "dev": "concurrently pnpm:dev:*", 11 | "lint:markup": "markuplint --config ../../.markuplintrc.cjs \"**\"", 12 | "lint:js": "eslint .", 13 | "lint:cspell": "cspell \"**\"", 14 | "lint:prettier": "prettier . --check --ignore-path=../../.prettierignore", 15 | "lint": "concurrently pnpm:lint:*", 16 | "test:watch": "vitest", 17 | "test": "vitest run", 18 | "format:js": "eslint --fix .", 19 | "format:prettier": "prettier . --write --ignore-path=../../.prettierignore", 20 | "format": "concurrently pnpm:format:*", 21 | "add-size-to-img": "node ./commands/add-size-to-img.js", 22 | "clean-image": "./commands/clean-image.sh", 23 | "deploy": "./commands/deploy.sh" 24 | }, 25 | "devDependencies": { 26 | "@repo/eslint-config": "workspace:*", 27 | "@tailwindcss/cli": "^4.1.3", 28 | "browser-sync": "^3.0.3", 29 | "image-size": "^1.1.1", 30 | "tailwindcss": "^4.1.3", 31 | "vitest": "^2.1.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/mockup/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagizmo/webapp-template/b34d56f5cac1916fb4fb53c4535258d2780c7956/apps/mockup/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/mockup/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagizmo/webapp-template/b34d56f5cac1916fb4fb53c4535258d2780c7956/apps/mockup/public/favicon.ico -------------------------------------------------------------------------------- /apps/mockup/public/images/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagizmo/webapp-template/b34d56f5cac1916fb4fb53c4535258d2780c7956/apps/mockup/public/images/ogp.png -------------------------------------------------------------------------------- /apps/mockup/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebApp Template (mockup) 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 37 | 38 | 39 | 40 |
41 |
44 |

47 | WebApp Template (mockup) 48 |

49 |

50 | A starting point for building a static site. 51 |

52 | 62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |

What's inside?

70 |
71 |

Uses

72 |
73 |
    74 |
  • 77 | Turborepo 84 | x 85 | pnpm 92 |
  • 93 |
  • 96 | Prettier 103 | (w/ 104 | 124 | ) 125 |
  • 126 |
  • 129 | ESLint 136 | / 137 | CSpell 144 |
  • 145 |
  • 148 | lint-staged 155 | / 156 | husky 163 |
  • 164 |
  • 167 | GitHub Actions ( 168 |
      169 |
    • Linting
    • 170 |
    • 171 | Testing (Validate 172 | `href` and 173 | `src` paths) 174 |
    • 175 |
    176 | ) 177 |
  • 178 |
  • 181 | Execute `eslint --fix` and 182 | `prettier` when saving with VS 183 | Code 184 |
  • 185 |
186 |
187 |
188 |
189 | 190 | 191 |
192 |

Apps and Packages

193 |
194 | 195 |
198 |

apps/

199 |
200 |
    201 |
  • 204 | backend
    205 | A 206 | Supabase 213 | Local Dev / CLI. 220 |
  • 221 |
  • 224 | mockup
    225 | A starting point for building a static site.
    226 |
      227 |
    • 228 | Tailwind CSS 235 |
    • 236 |
    • 237 | Vanilla JS 238 |
    • 239 |
    • 240 | Vitest 247 | ( 248 |
        249 |
      • 250 | Check links 251 |
      • 252 |
      • 253 | file names 254 |
      • 255 |
      256 | ) 257 |
    • 258 |
    259 |
  • 260 |
  • 263 | web
    264 | A starting point for building Svelte application.
    265 | SvelteKit 272 | (w/ 273 | 284 | )
    285 | Supabase 292 | / 293 | Vitest 300 |
  • 301 |
302 |
303 |
304 | 305 | 306 |
309 |

packages/

310 |
311 | 365 |
366 |
367 |
368 |
369 | 370 | 371 |
372 |

Commands

373 | 374 |
pnpm i  # Resolve dependency packages and prepare .env files
377 | # Then set up /.env
378 | 
379 | # Run command for each package (apps/ + packages/)
380 | pnpm build   # 
381 | pnpm lint    # root: cspell + prettier --check
382 | pnpm test    # 
383 | pnpm format  # root: Format project-words.txt + prettier --write
384 | 385 | 386 | 396 |
397 |
398 |
399 | 400 | 412 | 413 | 414 | 415 | 416 | -------------------------------------------------------------------------------- /apps/mockup/public/script.js: -------------------------------------------------------------------------------- 1 | console.log('script.js'); 2 | -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/add-size-to-img.test.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises'; 2 | 3 | import { dirname, join } from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | import { describe, expect, it } from 'vitest'; 6 | 7 | import { convert, deepReaddir } from '../../commands/utils'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | describe('@deepReaddir', async () => { 13 | const dirsPath = join(__dirname, 'dirs'); 14 | 15 | it('All files', async () => { 16 | const expected = [ 17 | join(dirsPath, 'dir0/dir00/00.html'), 18 | join(dirsPath, 'dir0/dir00/00.js'), 19 | join(dirsPath, 'dir1/1.html'), 20 | join(dirsPath, 'dir1/dir10/10.js'), 21 | ]; 22 | 23 | expect(await deepReaddir(dirsPath)).toStrictEqual(expected); 24 | }); 25 | 26 | it('Filtered by .html', async () => { 27 | const options = { ext: '.html' }; 28 | const expected = [join(dirsPath, 'dir0/dir00/00.html'), join(dirsPath, 'dir1/1.html')]; 29 | 30 | expect(await deepReaddir(dirsPath, options)).toStrictEqual(expected); 31 | }); 32 | 33 | it('Filtered by .js', async () => { 34 | const options = { ext: '.js' }; 35 | const expected = [join(dirsPath, 'dir0/dir00/00.js'), join(dirsPath, 'dir1/dir10/10.js')]; 36 | 37 | expect(await deepReaddir(dirsPath, options)).toStrictEqual(expected); 38 | }); 39 | }); 40 | 41 | describe('@convert', async () => { 42 | it('Add image sizes', async () => { 43 | const inputPath = join(__dirname, 'html/input.txt'); 44 | const expected = await readFile(join(__dirname, 'html/expected.txt'), 'utf8'); 45 | 46 | expect(await convert(inputPath)).toBe(expected); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/dirs/dir0/dir00/00.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagizmo/webapp-template/b34d56f5cac1916fb4fb53c4535258d2780c7956/apps/mockup/tests/add-size-to-img/dirs/dir0/dir00/00.html -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/dirs/dir0/dir00/00.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagizmo/webapp-template/b34d56f5cac1916fb4fb53c4535258d2780c7956/apps/mockup/tests/add-size-to-img/dirs/dir0/dir00/00.js -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/dirs/dir1/1.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagizmo/webapp-template/b34d56f5cac1916fb4fb53c4535258d2780c7956/apps/mockup/tests/add-size-to-img/dirs/dir1/1.html -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/dirs/dir1/dir10/10.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagizmo/webapp-template/b34d56f5cac1916fb4fb53c4535258d2780c7956/apps/mockup/tests/add-size-to-img/dirs/dir1/dir10/10.js -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/html/expected.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 39 | 44 | 49 | 55 | 61 | 67 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/html/images/sample.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 200 x 120 5 | 6 | -------------------------------------------------------------------------------- /apps/mockup/tests/add-size-to-img/html/input.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 39 | 44 | 49 | 55 | 61 | 67 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /apps/mockup/tests/external-links.txt: -------------------------------------------------------------------------------- 1 | https://cdn.jsdelivr.net/npm/yakuhanjp@4.1.1/dist/css/yakuhanjp.css 2 | https://cspell.org 3 | https://eslint.org 4 | https://fonts.googleapis.com 5 | https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Noto+Sans+JP:wght@100..900&display=swap 6 | https://fonts.gstatic.com 7 | https://github.com/gajus/eslint-plugin-jsdoc 8 | https://github.com/lydell/eslint-plugin-simple-import-sort 9 | https://github.com/okonet/lint-staged 10 | https://github.com/prettier/eslint-config-prettier 11 | https://github.com/sveltejs/eslint-plugin-svelte 12 | https://github.com/sveltejs/prettier-plugin-svelte 13 | https://github.com/tailwindlabs/prettier-plugin-tailwindcss 14 | https://github.com/typicode/husky 15 | https://github.com/usagizmo/webapp-template 16 | https://pnpm.io 17 | https://prettier.io 18 | https://supabase.com/docs/guides/cli 19 | https://supabase.io 20 | https://svelte.dev/docs/kit/ 21 | https://tailwindcss.com 22 | https://turborepo.org 23 | https://usagizmo.com 24 | https://vitest.dev 25 | https://webapp-template-mockup.usagizmo.com -------------------------------------------------------------------------------- /apps/mockup/tests/path.test.js: -------------------------------------------------------------------------------- 1 | import { access, readFile, writeFile } from 'node:fs/promises'; 2 | 3 | import { execSync } from 'child_process'; 4 | import { basename, dirname, join } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | import { describe, expect, it } from 'vitest'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | const rootDir = join(__dirname, '..'); 11 | 12 | const publicDir = join(rootDir, 'public'); // The root directory 13 | const targetDir = join(rootDir, 'public'); // The directory to be tested 14 | const distDir = join(rootDir, 'tests'); // The directory to output the test results 15 | 16 | const imageExtensions = ['jpg', 'png', 'webp']; 17 | const linkAttrs = ['href', 'src']; 18 | const linkRegex = new RegExp(`(?:${linkAttrs.join('|')})="([^"]+?)"`, 'g'); 19 | 20 | describe.concurrent('The tests (will be run in parallel)', () => { 21 | it(`Output external-links.txt`, async () => { 22 | let errorMessage = ''; 23 | 24 | const res = execSync(`find ${targetDir} -type f -name "*.html"`); 25 | const filePaths = res.toString().trim().split('\n'); 26 | 27 | /** @type {Set} */ 28 | const externalLinks = new Set(); 29 | 30 | /** 31 | * Add the path of the external link depending on the type 32 | */ 33 | const addToExternalLinks = { 34 | /** 35 | * Add the path of the external link 36 | * @param {string} path - The path of the external link 37 | */ 38 | external(path) { 39 | externalLinks.add(path); 40 | }, 41 | /** 42 | * Add the path of the external link if the file does not exist 43 | * @param {string} path - The path of the external link to check 44 | * @returns {Promise} 45 | */ 46 | async rootRelative(path) { 47 | try { 48 | await access(join(publicDir, path)); 49 | } catch { 50 | externalLinks.add(path); 51 | } 52 | }, 53 | /** 54 | * Add the path of the external link if the id does not exist 55 | * @param {string} path - The path of the external link to check 56 | * @param {string} text - The text of the html file 57 | */ 58 | hash(path, text) { 59 | const id = path.slice(1); 60 | const index = text.indexOf(`id="${id}"`); 61 | if (index === -1) { 62 | externalLinks.add(path); 63 | } 64 | }, 65 | /** 66 | * Add the path of the external link if the file does not exist 67 | * @param {string} path - The path of the external link to check 68 | * @param {string} filePath - The path of the html file 69 | * @returns {Promise} 70 | */ 71 | async relative(path, filePath) { 72 | try { 73 | const pathWithoutQuery = path.split('?')[0]; 74 | await access(join(dirname(filePath), pathWithoutQuery)); 75 | } catch { 76 | externalLinks.add(path); 77 | } 78 | }, 79 | }; 80 | 81 | for (const filePath of filePaths) { 82 | const text = await readFile(filePath, 'utf8'); 83 | await Promise.all( 84 | [...text.matchAll(linkRegex)].map((match) => { 85 | const path = match[1]; 86 | 87 | if (path.startsWith('http') || path.startsWith('//')) { 88 | return addToExternalLinks.external(path); 89 | } 90 | if (path.startsWith('/')) { 91 | return addToExternalLinks.rootRelative(path); 92 | } 93 | if (path.startsWith('#')) { 94 | return addToExternalLinks.hash(path, text); 95 | } 96 | return addToExternalLinks.relative(path, filePath); 97 | }), 98 | ); 99 | } 100 | 101 | const data = [...externalLinks].sort().join('\n'); 102 | 103 | let externalLinksText = ''; 104 | try { 105 | externalLinksText = await readFile(join(distDir, 'external-links.txt'), 'utf8'); 106 | } catch { 107 | // If external-links.txt does not exist 108 | try { 109 | await writeFile(join(distDir, 'external-links.txt'), data, 'utf8'); 110 | } catch (err) { 111 | errorMessage = err.toString(); 112 | } 113 | expect(errorMessage).toBe(''); 114 | return; 115 | } 116 | 117 | expect(data).toBe(externalLinksText); 118 | }); 119 | 120 | describe('All image file names are valid', async () => { 121 | const findImagesOption = imageExtensions.map((ext) => `-name "*.${ext}"`).join(' -o '); 122 | const res = execSync(`find ${targetDir} -type f ${findImagesOption}`); 123 | const filePaths = res.toString().trim().split('\n'); 124 | 125 | if (filePaths.length === 1 && filePaths[0] === '') { 126 | return; 127 | } 128 | 129 | const fileNames = filePaths.map((filePath) => basename(filePath)); 130 | it.each(fileNames)('%s', (fileName) => { 131 | const regex = new RegExp(`^[0-9a-z_-]+\\.(?:${imageExtensions.join('|')})$`); 132 | const isValid = regex.test(fileName); 133 | expect(isValid).toBe(true); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | 12 | # custom 13 | /src/lib/$generated/* 14 | !/src/lib/$generated/supabase-types.ts 15 | /supabase/.temp 16 | -------------------------------------------------------------------------------- /apps/web/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # `web` app 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | [[Demo](https://webapp-template.usagizmo.com/)] 6 | 7 | ## Commands 8 | 9 | ```bash 10 | pnpm generate # Copy Supabase types from backend/$generated to src/lib/$generated/ 11 | pnpm build # Output `.svelte-kit/output/` 12 | pnpm preview # Preview the production build (after `pnpm build`) 13 | 14 | pnpm dev # start the server and open the app in a new browser tab on port 5173 15 | pnpm lint # markuplint + cspell + eslint + prettier 16 | pnpm format # Format with eslint + prettier 17 | ``` 18 | 19 | You can preview the production build with `pnpm preview`. 20 | 21 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 22 | 23 | ## Deploy to Vercel (apps/web) 24 | 25 | - Framework Preset: `SvelteKit` 26 | - Root Directory: `apps/web` 27 | - Environment Variables: Set 2 environment variables in `.env` 28 | - Corepack Configuration: Add the following environment variable to enable pnpm@10: 29 | - Key: `ENABLE_EXPERIMENTAL_COREPACK` 30 | - Value: `1` 31 | -------------------------------------------------------------------------------- /apps/web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from '@repo/eslint-config'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "generate:supabase-types": "mkdir -p ./src/lib/\\$generated && cp ../backend/\\$generated/supabase-types.ts ./src/lib/\\$generated/supabase-types.ts", 8 | "generate": "concurrently pnpm:generate:*", 9 | "dev": "vite dev --open", 10 | "build": "vite build", 11 | "preview": "vite preview", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "lint:check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 14 | "lint:markup": "markuplint --config ../../.markuplintrc.cjs \"**\"", 15 | "lint:js": "eslint .", 16 | "lint:cspell": "cspell \"**\"", 17 | "lint:prettier": "prettier . --check --ignore-path=../../.prettierignore", 18 | "lint": "concurrently pnpm:lint:*", 19 | "test:watch": "vitest", 20 | "test": "vitest run", 21 | "format:js": "eslint --fix .", 22 | "format:prettier": "prettier . --write --ignore-path=../../.prettierignore", 23 | "format": "concurrently pnpm:format:*" 24 | }, 25 | "dependencies": { 26 | "@supabase/supabase-js": "^2.45.6", 27 | "camelcase-keys": "^9.1.3", 28 | "cdate": "^0.0.7", 29 | "snakecase-keys": "^8.0.1", 30 | "tailwind-variants": "^0.2.1" 31 | }, 32 | "devDependencies": { 33 | "@repo/eslint-config": "workspace:*", 34 | "@sveltejs/adapter-auto": "^3.3.0", 35 | "@sveltejs/kit": "^2.12.1", 36 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 37 | "@tailwindcss/vite": "^4.1.3", 38 | "svelte": "^5.16.0", 39 | "svelte-check": "^4.0.5", 40 | "tailwindcss": "^4.1.3", 41 | "tslib": "^2.8.0", 42 | "typescript": "^5.6.3", 43 | "vite": "^5.4.10", 44 | "vitest": "^2.1.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/src/app.css: -------------------------------------------------------------------------------- 1 | /* ref. https://tailwindcss.com/docs/theme */ 2 | @import 'tailwindcss'; 3 | 4 | @theme { 5 | /* --font-sans: -apple-system, blinkMacSystemFont, Helvetica, 'Yu Gothic', YuGothic, 6 | 'BIZ UDPGothic', Meiryo, sans-serif; 7 | --font-sans: -apple-system, blinkMacSystemFont, Helvetica, 'Hiragino Sans', 8 | 'Hiragino Kaku Gothic ProN', 'BIZ UDPGothic', Meiryo, sans-serif; 9 | --font-serif: 'Yu Mincho', YuMincho, 'Hiragino Mincho ProN', serif; */ 10 | --font-sans: YakuHanJP, 'Noto Sans JP', sans-serif; 11 | --font-ui: Inter, YakuHanJP, 'Noto Sans JP', sans-serif; 12 | --font-mono: 'JetBrains Mono', monospace; 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 18 | %sveltekit.head% 19 | 20 | 21 |
%sveltekit.body%
22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/web/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/$generated/supabase-types.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json | undefined } 7 | | Json[] 8 | 9 | export type Database = { 10 | graphql_public: { 11 | Tables: { 12 | [_ in never]: never 13 | } 14 | Views: { 15 | [_ in never]: never 16 | } 17 | Functions: { 18 | graphql: { 19 | Args: { 20 | operationName?: string 21 | query?: string 22 | variables?: Json 23 | extensions?: Json 24 | } 25 | Returns: Json 26 | } 27 | } 28 | Enums: { 29 | [_ in never]: never 30 | } 31 | CompositeTypes: { 32 | [_ in never]: never 33 | } 34 | } 35 | public: { 36 | Tables: { 37 | comments: { 38 | Row: { 39 | created_at: string 40 | file_path: string | null 41 | id: number 42 | text: string 43 | user_id: string 44 | } 45 | Insert: { 46 | created_at?: string 47 | file_path?: string | null 48 | id?: number 49 | text?: string 50 | user_id: string 51 | } 52 | Update: { 53 | created_at?: string 54 | file_path?: string | null 55 | id?: number 56 | text?: string 57 | user_id?: string 58 | } 59 | Relationships: [ 60 | { 61 | foreignKeyName: "public_comments_user_id_fkey" 62 | columns: ["user_id"] 63 | isOneToOne: false 64 | referencedRelation: "profiles" 65 | referencedColumns: ["id"] 66 | }, 67 | ] 68 | } 69 | profiles: { 70 | Row: { 71 | bio: string 72 | created_at: string 73 | display_name: string 74 | email: string 75 | id: string 76 | } 77 | Insert: { 78 | bio?: string 79 | created_at?: string 80 | display_name?: string 81 | email: string 82 | id: string 83 | } 84 | Update: { 85 | bio?: string 86 | created_at?: string 87 | display_name?: string 88 | email?: string 89 | id?: string 90 | } 91 | Relationships: [] 92 | } 93 | } 94 | Views: { 95 | [_ in never]: never 96 | } 97 | Functions: { 98 | [_ in never]: never 99 | } 100 | Enums: { 101 | [_ in never]: never 102 | } 103 | CompositeTypes: { 104 | [_ in never]: never 105 | } 106 | } 107 | } 108 | 109 | type PublicSchema = Database[Extract] 110 | 111 | export type Tables< 112 | PublicTableNameOrOptions extends 113 | | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) 114 | | { schema: keyof Database }, 115 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database } 116 | ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & 117 | Database[PublicTableNameOrOptions["schema"]]["Views"]) 118 | : never = never, 119 | > = PublicTableNameOrOptions extends { schema: keyof Database } 120 | ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & 121 | Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { 122 | Row: infer R 123 | } 124 | ? R 125 | : never 126 | : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & 127 | PublicSchema["Views"]) 128 | ? (PublicSchema["Tables"] & 129 | PublicSchema["Views"])[PublicTableNameOrOptions] extends { 130 | Row: infer R 131 | } 132 | ? R 133 | : never 134 | : never 135 | 136 | export type TablesInsert< 137 | PublicTableNameOrOptions extends 138 | | keyof PublicSchema["Tables"] 139 | | { schema: keyof Database }, 140 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database } 141 | ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] 142 | : never = never, 143 | > = PublicTableNameOrOptions extends { schema: keyof Database } 144 | ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { 145 | Insert: infer I 146 | } 147 | ? I 148 | : never 149 | : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] 150 | ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { 151 | Insert: infer I 152 | } 153 | ? I 154 | : never 155 | : never 156 | 157 | export type TablesUpdate< 158 | PublicTableNameOrOptions extends 159 | | keyof PublicSchema["Tables"] 160 | | { schema: keyof Database }, 161 | TableName extends PublicTableNameOrOptions extends { schema: keyof Database } 162 | ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] 163 | : never = never, 164 | > = PublicTableNameOrOptions extends { schema: keyof Database } 165 | ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { 166 | Update: infer U 167 | } 168 | ? U 169 | : never 170 | : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] 171 | ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { 172 | Update: infer U 173 | } 174 | ? U 175 | : never 176 | : never 177 | 178 | export type Enums< 179 | PublicEnumNameOrOptions extends 180 | | keyof PublicSchema["Enums"] 181 | | { schema: keyof Database }, 182 | EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } 183 | ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] 184 | : never = never, 185 | > = PublicEnumNameOrOptions extends { schema: keyof Database } 186 | ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] 187 | : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] 188 | ? PublicSchema["Enums"][PublicEnumNameOrOptions] 189 | : never 190 | 191 | export type CompositeTypes< 192 | PublicCompositeTypeNameOrOptions extends 193 | | keyof PublicSchema["CompositeTypes"] 194 | | { schema: keyof Database }, 195 | CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { 196 | schema: keyof Database 197 | } 198 | ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] 199 | : never = never, 200 | > = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } 201 | ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] 202 | : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"] 203 | ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] 204 | : never 205 | 206 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/Input.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 | 38 | {#if error.required && isDirty && !value} 39 |
{error.required}
40 | {/if} 41 |
42 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/Meta.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | {title} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {#if canonical} 35 | 36 | {/if} 37 | 38 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/TextArea.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | 38 | {#if error.required && isDirty && !value} 39 |
{error.required}
40 | {/if} 41 |
42 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/icons/16x16/PaperPlaneIcon.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/icons/16x16/SignInIcon.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/icons/16x16/SignOutIcon.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/icons/20x20/CircleCheckIcon.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/icons/20x20/CircleCloseIcon.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /apps/web/src/lib/easing.ts: -------------------------------------------------------------------------------- 1 | import { quintOut } from 'svelte/easing'; 2 | 3 | export const defaultDE = { 4 | duration: 450, 5 | easing: quintOut, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/src/lib/features/comment/Comment.svelte: -------------------------------------------------------------------------------- 1 | 96 | 97 |
(isActionVisible = card.me ? true : false)} 101 | onmouseleave={() => (isActionVisible = false)} 102 | > 103 |
104 |
105 |

{card.name}

106 | {#if card.me} 107 |
108 | 109 |
110 | {/if} 111 | 117 |
118 |
119 |

{card.message}

120 | {#if card.filePath} 121 | {@const commentFileUrl = getCommentFileUrl(card.filePath)} 122 |
123 |
124 | 125 |
126 | {#if isActionVisible} 127 | 134 | {/if} 135 |
136 | {/if} 137 |
138 | 139 | {#if isActionVisible} 140 |
141 | 143 |
144 | {/if} 145 |
146 |
147 | -------------------------------------------------------------------------------- /apps/web/src/lib/features/comment/Comments.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | {#if commentStore.isLoading} 13 |
14 |
15 |
16 | {:else} 17 | {#each commentStore.comments as comment (comment.id)} 18 |
19 | 20 |
21 | {/each} 22 | {/if} 23 |
24 | -------------------------------------------------------------------------------- /apps/web/src/lib/features/comment/commentRequests.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '$lib/supabase'; 2 | 3 | /** 4 | * Uploads a file to the comments storage bucket 5 | * @param uid user id 6 | * @param file file to upload 7 | * @returns file path and error 8 | */ 9 | export async function uploadCommentFile(uid: string, file: File) { 10 | const path = `${uid}/${Date.now()}_${file.name}`; 11 | return supabase.storage.from('comments').upload(path, file); 12 | } 13 | 14 | /** 15 | * Deletes a file from the comments storage bucket 16 | * @param filePath path to the file to delete 17 | * @returns file object and error 18 | */ 19 | export async function deleteCommentFile(filePath: string) { 20 | return supabase.storage.from('comments').remove([filePath]); 21 | } 22 | 23 | /** 24 | * Gets the public URL of a file in the comments storage bucket 25 | * @param path path to the file 26 | * @returns public URL of the file 27 | */ 28 | export function getCommentFileUrl(path: string): string { 29 | const { 30 | data: { publicUrl }, 31 | } = supabase.storage.from('comments').getPublicUrl(path); 32 | return publicUrl; 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/lib/features/comment/commentStore.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { PostgrestError } from '@supabase/supabase-js'; 2 | 3 | import { userStore } from '$lib/features/user/userStore.svelte'; 4 | import { supabase } from '$lib/supabase'; 5 | 6 | import { deleteCommentFile, uploadCommentFile } from './commentRequests'; 7 | 8 | export interface Comment { 9 | id: number; 10 | profiles: { 11 | id: string; 12 | display_name: string; 13 | } | null; 14 | text: string; 15 | file_path: string | null; 16 | created_at: string; 17 | } 18 | 19 | export const commentQuery = ` 20 | id, 21 | profiles ( 22 | id, 23 | display_name 24 | ), 25 | text, 26 | file_path, 27 | created_at 28 | `; 29 | 30 | export class CommentStore { 31 | #isLoading = $state(false); 32 | #comments = $state([]); 33 | 34 | get comments() { 35 | return this.#comments; 36 | } 37 | 38 | get isLoading() { 39 | return this.#isLoading; 40 | } 41 | 42 | async fetchComments() { 43 | this.#isLoading = true; 44 | 45 | const { data, error } = await supabase 46 | .from('comments') 47 | .select(commentQuery) 48 | .order('created_at', { ascending: false }); 49 | 50 | if (error) throw error; 51 | 52 | this.#comments = data; 53 | this.#isLoading = false; 54 | } 55 | 56 | async insertComment({ text, file }: { text: string; file?: File }): Promise<{ 57 | error: PostgrestError | Error | null; 58 | }> { 59 | if (!userStore.user) { 60 | return { error: new Error('You must be logged in to comment') }; 61 | } 62 | 63 | let file_path: string | null = null; 64 | 65 | if (file) { 66 | const { data, error } = await uploadCommentFile(userStore.user.id, file); 67 | if (error) return { error }; 68 | file_path = data.path; 69 | } 70 | 71 | const { data, error } = await supabase 72 | .from('comments') 73 | .insert([{ text, file_path, user_id: userStore.user.id }]) 74 | .select(commentQuery); 75 | if (error) return { error }; 76 | 77 | this.#comments.unshift(data[0]); 78 | return { error: null }; 79 | } 80 | 81 | async updateComment( 82 | id: number, 83 | props: { file_path?: string | null }, 84 | ): Promise<{ error: PostgrestError | null }> { 85 | const { error } = await supabase.from('comments').update(props).eq('id', id); 86 | if (error) return { error }; 87 | 88 | this.#comments = this.#comments.map((comment) => { 89 | if (comment.id === id) { 90 | return { ...comment, ...props }; 91 | } 92 | return comment; 93 | }); 94 | 95 | return { error: null }; 96 | } 97 | 98 | async deleteComment( 99 | id: number, 100 | filePath: string | null, 101 | ): Promise<{ error: PostgrestError | Error | null }> { 102 | if (filePath) { 103 | const { error } = await deleteCommentFile(filePath); 104 | if (error) return { error }; 105 | } 106 | 107 | const { error } = await supabase.from('comments').delete().eq('id', id); 108 | if (error) return { error }; 109 | 110 | this.#comments = this.#comments.filter((comment) => comment.id !== id); 111 | return { error: null }; 112 | } 113 | } 114 | 115 | export const commentStore = new CommentStore(); 116 | -------------------------------------------------------------------------------- /apps/web/src/lib/features/user/OnAuthStateChange.svelte: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /apps/web/src/lib/features/user/userRequests.ts: -------------------------------------------------------------------------------- 1 | import type { AuthError, PostgrestError } from '@supabase/supabase-js'; 2 | import camelcaseKeys from 'camelcase-keys'; 3 | 4 | import { supabase } from '$lib/supabase'; 5 | 6 | import type { User } from './userStore.svelte'; 7 | 8 | /** 9 | * Sign up a user 10 | * @param inputs User inputs 11 | * @param inputs.email User email 12 | * @param inputs.password User password 13 | * @param inputs.displayName User display name 14 | * @returns Error 15 | */ 16 | export async function signUp(inputs: { 17 | email: string; 18 | password: string; 19 | displayName: string; 20 | }): Promise<{ error: AuthError | null }> { 21 | const { email, password, displayName } = inputs; 22 | const { error } = await supabase.auth.signUp({ 23 | email, 24 | password, 25 | options: { 26 | data: { display_name: displayName }, 27 | }, 28 | }); 29 | return { error }; 30 | } 31 | 32 | /** 33 | * Sign in a user 34 | * @param inputs User inputs 35 | * @param inputs.email User email 36 | * @param inputs.password User password 37 | * @returns Error 38 | */ 39 | export async function signIn(inputs: { 40 | email: string; 41 | password: string; 42 | }): Promise<{ error: AuthError | null }> { 43 | const { error } = await supabase.auth.signInWithPassword(inputs); 44 | return { error }; 45 | } 46 | 47 | /** 48 | * Sign out a user 49 | * @returns Error 50 | */ 51 | export async function signOut(): Promise<{ error: AuthError | null }> { 52 | const { error } = await supabase.auth.signOut(); 53 | return { error }; 54 | } 55 | 56 | /** 57 | * Get a user (profile) by id 58 | * @param id user id 59 | * @returns user (profile) and error 60 | */ 61 | export async function getUser( 62 | id: string, 63 | ): Promise<{ user: User | null; error: PostgrestError | null }> { 64 | const { data: profiles, error } = await supabase 65 | .from('profiles') 66 | .select('id, email, display_name, bio, created_at') 67 | .eq('id', id); 68 | 69 | const user = profiles?.length ? (camelcaseKeys(profiles[0]) satisfies User) : null; 70 | 71 | return { user, error }; 72 | } 73 | -------------------------------------------------------------------------------- /apps/web/src/lib/features/user/userStore.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { PostgrestError } from '@supabase/supabase-js'; 2 | import snakecaseKeys from 'snakecase-keys'; 3 | 4 | import { supabase } from '$lib/supabase'; 5 | 6 | export interface User { 7 | id: string; 8 | email: string; 9 | displayName: string; 10 | bio: string; 11 | createdAt: string; 12 | } 13 | 14 | class UserStore { 15 | #user = $state(null); 16 | 17 | /** 18 | * Get the user 19 | * @returns The user 20 | */ 21 | get user(): User | null { 22 | return this.#user; 23 | } 24 | 25 | /** 26 | * Set the user 27 | */ 28 | set user(user: User | null) { 29 | this.#user = user; 30 | } 31 | 32 | async updateUser( 33 | id: string, 34 | props: Partial>, 35 | ): Promise<{ error: PostgrestError | Error | null }> { 36 | if (!this.#user) return { error: new Error('You must be logged in to update your profile') }; 37 | 38 | // NOTE: $state.snapshot is used to get the current value of the reactive variable 39 | const plainSnakeProps = snakecaseKeys($state.snapshot(props)); 40 | const { error } = await supabase.from('profiles').update(plainSnakeProps).eq('id', id); 41 | if (error) return { error }; 42 | 43 | this.#user = { ...this.#user, ...props }; 44 | 45 | return { error: null }; 46 | } 47 | } 48 | 49 | export const userStore = new UserStore(); 50 | -------------------------------------------------------------------------------- /apps/web/src/lib/routes.ts: -------------------------------------------------------------------------------- 1 | export const ROUTE = { 2 | HOME: '/', 3 | ADMIN: '/admin', 4 | ADMIN_LOGIN: '/admin/login', 5 | ADMIN_SIGNUP: '/admin/signup', 6 | SECRET: '/secret', 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | 3 | import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'; 4 | 5 | import type { Database } from './$generated/supabase-types'; 6 | 7 | export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY); 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/variants/buttonVariants.ts: -------------------------------------------------------------------------------- 1 | import { tv } from 'tailwind-variants'; 2 | 3 | export const buttonVariants = tv({ 4 | base: 'font-ui inline-flex items-center justify-center space-x-1 rounded-md border px-5 py-2 text-sm duration-200 disabled:pointer-events-none disabled:opacity-40', 5 | variants: { 6 | primary: { 7 | true: 'border-zinc-900 bg-zinc-900 text-white hover:bg-zinc-700', 8 | false: 'border-zinc-300 bg-slate-50 hover:border-zinc-400 hover:bg-slate-100', 9 | }, 10 | }, 11 | defaultVariants: { 12 | primary: false, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/web/src/lib/variants/sectionFrameVariants.ts: -------------------------------------------------------------------------------- 1 | import { tv } from 'tailwind-variants'; 2 | 3 | export const sectionFrameVariants = tv({ 4 | base: 'rounded-lg bg-slate-50', 5 | variants: { 6 | pad: { 7 | x: 'px-6', 8 | xb: 'px-6 pb-6', 9 | default: 'p-6', 10 | }, 11 | }, 12 | defaultVariants: { 13 | pad: 'default', 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /apps/web/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | {#if PUBLIC_GA4_MEASUREMENT_ID} 21 | 22 | {/if} 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | {@render children()} 33 |
34 | 35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /apps/web/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 |
27 | {#if userStore.user} 28 | 29 | {:else} 30 | 31 | {/if} 32 | 33 |
34 |
35 | 36 |
37 |

Comments will be deleted as appropriate.

38 |
39 | 40 | 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /apps/web/src/routes/CommentForm.svelte: -------------------------------------------------------------------------------- 1 | 63 | 64 |
65 |
66 |
67 | 75 | 97 |
98 |
99 | 107 |
108 |
109 |
110 | -------------------------------------------------------------------------------- /apps/web/src/routes/Footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /apps/web/src/routes/GA4.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /apps/web/src/routes/HeaderNavigation.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |
26 |

29 | WebApp Template (web) 30 |

31 |
34 | 35 |
36 |
37 | GitHub 43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /apps/web/src/routes/HeaderNavigationItems.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
    27 | {#each navItems as { label, href }} 28 | {@const isActive = 29 | href === ROUTE.HOME 30 | ? href === page.url.pathname 31 | : getScope(page.url.pathname) === getScope(href)} 32 |
  • 33 | {#if isActive} 34 | {label} 35 | {:else} 36 | {label} 41 | {/if} 42 | {#if isActive} 43 | 48 | {/if} 49 |
  • 50 | {/each} 51 |
52 | -------------------------------------------------------------------------------- /apps/web/src/routes/LoginMessage.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 10 |
11 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/(isNotLoggedIn)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 |
28 | 29 | 30 | 31 | {@render children()} 32 |
33 |
34 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/(isNotLoggedIn)/UserInputs.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 |
21 | {#if isSignUpPage} 22 |
23 | (userInputs.displayName = (event.target as HTMLInputElement).value)} 28 | error={{ required: 'Display Name is required.' }} 29 | /> 30 |
31 | {/if} 32 | (userInputs.email = (event.target as HTMLInputElement).value)} 37 | error={{ required: 'E-mail is required.' }} 38 | /> 39 | (userInputs.password = (event.target as HTMLInputElement).value)} 44 | error={{ required: 'Password is required.' }} 45 | /> 46 |
47 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/(isNotLoggedIn)/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 |
24 |
25 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/(isNotLoggedIn)/signup/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 |
24 |
25 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/+layout.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | {@render children()} 13 |
14 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | {#if userStore.user} 17 | 18 | {:else} 19 | 20 | {/if} 21 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/AdminHeaderMessage.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 |
25 |
26 | {#if isSignUpPage} 27 |
28 |

29 | You can register as a member
30 | with an irresponsible email and password. 31 |

32 |

No email will be sent.

33 |
34 | {:else} 35 |
36 |

Guest account

37 |
38 |
39 |
email:
40 |
email@add.com
41 |
42 |
43 |
pass:
44 |
password0
45 |
46 |
47 |
48 | {/if} 49 |
50 |
51 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/AdminHeaderTabs.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |
    24 | {#each tabs as { name, href }} 25 | {@const isActive = href === page.url.pathname} 26 |
  • 27 | {#if isActive} 28 | 33 | {/if} 34 | 45 | {name} 46 | 47 |
  • 48 | {/each} 49 |
50 |
51 | -------------------------------------------------------------------------------- /apps/web/src/routes/admin/LoggedInMessage.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
39 |
40 |

{user.displayName}

41 |

{user.email}

42 |
43 |