├── .env.example
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .storybook
├── Wrapper.svelte
├── main.ts
├── preview-head.html
└── preview.ts
├── .typesafe-i18n.json
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── prisma
├── migrations
│ ├── 20221220114054_init
│ │ └── migration.sql
│ └── migration_lock.toml
├── schema.prisma
└── seed.ts
├── src
├── app.d.ts
├── app.html
├── assets
│ └── bigben.avif
├── helpers
│ ├── scripts
│ │ └── trpc.ts
│ └── styles
│ │ ├── a11y.scss
│ │ ├── main.scss
│ │ └── variables.scss
├── hooks.server.ts
├── i18n
│ ├── de
│ │ └── index.ts
│ ├── en
│ │ └── index.ts
│ ├── formatters.ts
│ ├── i18n-svelte.ts
│ ├── i18n-types.ts
│ ├── i18n-util.async.ts
│ ├── i18n-util.sync.ts
│ └── i18n-util.ts
├── lib
│ ├── components
│ │ ├── _reusables
│ │ │ └── .gitkeep
│ │ ├── atoms
│ │ │ ├── .gitkeep
│ │ │ ├── Button
│ │ │ │ ├── Button.stories.svelte
│ │ │ │ └── Button.svelte
│ │ │ └── Logo
│ │ │ │ ├── Logo.stories.svelte
│ │ │ │ ├── Logo.svelte
│ │ │ │ └── svelte.svg
│ │ ├── molecules
│ │ │ └── .gitkeep
│ │ └── organisms
│ │ │ └── .gitkeep
│ └── server
│ │ ├── prisma.ts
│ │ └── trpc
│ │ ├── _app.ts
│ │ ├── createContext.ts
│ │ ├── middleware
│ │ └── isAuthenticated.ts
│ │ └── server.ts
└── routes
│ ├── (api)
│ └── trpc
│ │ └── [...args]
│ │ └── +server.ts
│ └── (app)
│ ├── +layout.server.ts
│ ├── +layout.svelte
│ ├── +page.svelte
│ └── [locale]
│ ├── +page.server.ts
│ └── +page.svelte
├── static
└── favicon.png
├── svelte.config.js
├── tsconfig.json
├── types
├── HTMLImageElement.d.ts
├── poppanator-sveltekit-svg.d.ts
└── sveltejs-enhanced-img.d.ts
└── vite.config.js
/.env.example:
--------------------------------------------------------------------------------
1 | # Environment variables declared in this file are automatically made available to Prisma.
2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
3 |
4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6 |
7 | DATABASE_URL="file:./dev.db"
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:storybook/recommended'],
5 | plugins: ['svelte', '@typescript-eslint'],
6 | ignorePatterns: ['*.cjs'],
7 | overrides: [{
8 | files: ["*.svelte"],
9 | parser: "svelte-eslint-parser",
10 | parserOptions: {
11 | parser: "@typescript-eslint/parser",
12 | },
13 | }],
14 | };
--------------------------------------------------------------------------------
/.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 | prisma/*.db
12 | prisma/*.db-journal
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore PNPM, NPM and YARN files
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte"],
7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
8 | }
9 |
--------------------------------------------------------------------------------
/.storybook/Wrapper.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from '@storybook/sveltekit';
2 | const config: StorybookConfig = {
3 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'],
4 | addons: [
5 | '@storybook/addon-links',
6 | '@storybook/addon-essentials',
7 | '@storybook/addon-interactions',
8 | '@storybook/addon-svelte-csf'
9 | ],
10 | framework: {
11 | name: '@storybook/sveltekit',
12 | options: {}
13 | },
14 | docs: {
15 | autodocs: 'tag'
16 | }
17 | };
18 | export default config;
19 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import type { Preview } from '@storybook/svelte';
2 | import Wrapper from './Wrapper.svelte';
3 |
4 | const preview: Preview = {
5 | parameters: {
6 | actions: { argTypesRegex: '^on[A-Z].*' },
7 | controls: {
8 | matchers: {
9 | color: /(background|color)$/i,
10 | date: /Date$/
11 | }
12 | }
13 | },
14 | // @ts-ignore
15 | decorators: [() => Wrapper]
16 | };
17 |
18 | export default preview;
19 |
--------------------------------------------------------------------------------
/.typesafe-i18n.json:
--------------------------------------------------------------------------------
1 | {
2 | "adapter": "svelte",
3 | "esmImports": true,
4 | "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
5 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20
2 |
3 | WORKDIR /app
4 |
5 | COPY /.npmrc /app/
6 | COPY /package*.json /app/
7 | RUN npm ci
8 |
9 | COPY /static /app/static
10 | COPY postcss.config.cjs /app/
11 | COPY svelte.config.js /app/
12 | COPY vite.config.js /app/
13 | COPY /.storybook /app/.storybook
14 | COPY tsconfig.json /app/
15 |
16 | COPY /.env.production /app/.env
17 |
18 | COPY /prisma /app/prisma
19 | RUN npx -y prisma generate
20 |
21 | COPY /src /app/src
22 | RUN npm run build
23 |
24 | CMD [ "node", "/app/build/index.js" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Luca Goslar
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 | # sveltekit-fullstack
2 |
3 | Everything you need to build a Svelte project with [Storybook](https://storybook.js.org/), [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n), [Prisma](https://prisma.io/) and [trpc](https://trpc.io/).
4 |
5 | ## Developing
6 |
7 | Make sure to create a copy of `.env.example` with the name `.env` and adapt it to your requirements before running the application.
8 |
9 | ```bash
10 | # install dependencies
11 | npm i
12 |
13 | # apply db migrations to db
14 | npx prisma migrate dev
15 |
16 | # seed the database (flags '--no-flush' and '--no-seed' available)
17 | npm run seed --
18 |
19 | # run storybook
20 | npm run storybook
21 |
22 | # or run the development server
23 | npm run dev
24 | ```
25 |
26 | ## Building
27 |
28 | You may build for any target wanted. However, this project is preconfigured to operate on Docker. Similar to before, create a copy of `.env.example`. However, name it `.env.production` this time. Take into consideration that your application will use port `3000` in production. Before starting the service, apply any pending migrations with `prisma migrate deploy` to your database.
29 |
30 | ```bash
31 | # build and run the image
32 | docker-compose up --build
33 | ```
34 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | web:
5 | build: .
6 | ports:
7 | - '3000:3000'
8 | env_file:
9 | - .env.production
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sveltekit-fullstack",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "concurrently \"typesafe-i18n\" \"vite dev\"",
7 | "build": "typesafe-i18n --no-watch && vite build",
8 | "preview": "vite preview",
9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11 | "lint": "prettier --plugin-search-dir . --check . && eslint .",
12 | "format": "prettier --plugin-search-dir . --write .",
13 | "typesafe-i18n": "typesafe-i18n",
14 | "storybook": "storybook dev -p 6006",
15 | "build-storybook": "storybook build",
16 | "seed": "ts-node-esm prisma/seed.ts"
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.23.9",
20 | "@poppanator/sveltekit-svg": "^4.2.1",
21 | "@storybook/addon-actions": "^7.6.12",
22 | "@storybook/addon-essentials": "^7.6.12",
23 | "@storybook/addon-interactions": "^7.6.12",
24 | "@storybook/addon-links": "^7.6.12",
25 | "@storybook/addon-svelte-csf": "^4.1.0",
26 | "@storybook/blocks": "^7.6.12",
27 | "@storybook/builder-vite": "^7.6.12",
28 | "@storybook/cli": "^7.6.12",
29 | "@storybook/svelte": "^7.6.12",
30 | "@storybook/sveltekit": "^7.6.12",
31 | "@storybook/testing-library": "^0.2.2",
32 | "@sveltejs/adapter-auto": "^3.1.1",
33 | "@sveltejs/adapter-node": "^4.0.1",
34 | "@sveltejs/enhanced-img": "^0.1.8",
35 | "@sveltejs/kit": "^2.5.0",
36 | "@sveltejs/vite-plugin-svelte": "^3.0.2",
37 | "@types/accept-language-parser": "^1.5.6",
38 | "@typescript-eslint/eslint-plugin": "^6.20.0",
39 | "@typescript-eslint/parser": "^6.20.0",
40 | "autoprefixer": "^10.4.17",
41 | "babel-loader": "^9.1.3",
42 | "concurrently": "^8.2.2",
43 | "dotenv": "^16.4.1",
44 | "eslint": "^8.56.0",
45 | "eslint-config-prettier": "^9.1.0",
46 | "eslint-plugin-storybook": "^0.6.15",
47 | "eslint-plugin-svelte": "^2.35.1",
48 | "imagetools-core": "^6.0.4",
49 | "postcss-pxtorem": "^6.1.0",
50 | "prettier": "^3.2.4",
51 | "prettier-plugin-svelte": "^3.1.2",
52 | "prisma": "^5.9.0",
53 | "react": "^18.2.0",
54 | "react-dom": "^18.2.0",
55 | "sass": "^1.70.0",
56 | "storybook": "^7.6.12",
57 | "svelte": "^4.2.9",
58 | "svelte-check": "^3.6.3",
59 | "svelte-loader": "^3.1.9",
60 | "svelte-preprocess-sass-alias-import": "^1.0.0",
61 | "svgo": "^3.2.0",
62 | "ts-node": "^10.9.2",
63 | "tslib": "^2.6.2",
64 | "typescript": "^5.3.3",
65 | "vite": "^5.0.12"
66 | },
67 | "type": "module",
68 | "dependencies": {
69 | "@prisma/client": "^5.9.0",
70 | "@trpc/client": "^10.45.0",
71 | "@trpc/server": "^10.45.0",
72 | "accept-language-parser": "^1.5.0",
73 | "normalize.css": "^8.0.1",
74 | "svelte-meta-tags": "^3.1.0",
75 | "trpc-sveltekit": "^3.5.26",
76 | "typesafe-i18n": "^5.26.2",
77 | "zod": "^3.22.4"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const autoprefixer = require('autoprefixer');
2 | const pxtorem = require('postcss-pxtorem');
3 |
4 | const config = {
5 | plugins: [
6 | pxtorem({
7 | rootValue: 16,
8 | unitPrecision: 5,
9 | propList: ['*'],
10 | replace: true,
11 | mediaQuery: true,
12 | minPixelValue: 0,
13 | exclude: /node_modules/i
14 | }),
15 | autoprefixer
16 | ]
17 | };
18 |
19 | module.exports = config;
20 |
--------------------------------------------------------------------------------
/prisma/migrations/20221220114054_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4 | "email" TEXT NOT NULL,
5 | "name" TEXT
6 | );
7 |
8 | -- CreateIndex
9 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
10 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "sqlite"
10 | url = env("DATABASE_URL")
11 | }
12 |
13 | model User {
14 | id Int @id @default(autoincrement())
15 | email String @unique
16 | name String?
17 | }
--------------------------------------------------------------------------------
/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import { PrismaClient } from '@prisma/client';
3 |
4 | dotenv.config();
5 |
6 | const prisma = new PrismaClient({
7 | datasources: {
8 | db: {
9 | url: process.env.DATABASE_URL
10 | }
11 | }
12 | });
13 |
14 | async function seed() {
15 | await prisma.$transaction([prisma.user.create({ data: { email: 'mail@example.com' } })]);
16 | console.log('Database seeded.');
17 | }
18 |
19 | async function flush() {
20 | await prisma.$transaction([prisma.user.deleteMany()]);
21 |
22 | console.log('Database flushed.');
23 | }
24 |
25 | if (!process.argv.includes('--no-flush')) {
26 | await flush();
27 | }
28 |
29 | if (!process.argv.includes('--no-seed')) {
30 | await seed();
31 | }
32 |
33 | console.log('All set.');
34 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | // and what to do when importing types
4 | declare namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/bigben.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/ad2e43a8917414d4800158b959e32b808522b649/src/assets/bigben.avif
--------------------------------------------------------------------------------
/src/helpers/scripts/trpc.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCClient, type TRPCClientInit } from 'trpc-sveltekit';
2 | import type { AppRouter } from '$lib/server/trpc/_app';
3 |
4 | let browserClient: ReturnType>;
5 |
6 | const slug = '/trpc';
7 |
8 | export const client = (init?: TRPCClientInit) => {
9 | if (typeof window === 'undefined') {
10 | return createTRPCClient({
11 | url: slug,
12 | init
13 | });
14 | }
15 |
16 | if (!browserClient)
17 | browserClient = createTRPCClient({
18 | url: slug
19 | });
20 |
21 | return browserClient;
22 | };
23 |
--------------------------------------------------------------------------------
/src/helpers/styles/a11y.scss:
--------------------------------------------------------------------------------
1 | @mixin outline($color: #000, $offset: 0) {
2 | &:focus-visible {
3 | outline: rgba($color, 0.3) solid 3px;
4 | }
5 |
6 | &:focus-visible:active {
7 | outline: rgba($color, 0.5) solid 3px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/styles/main.scss:
--------------------------------------------------------------------------------
1 | @use '$styles/variables.scss';
2 |
3 | * {
4 | font-family:
5 | system-ui,
6 | -apple-system,
7 | BlinkMacSystemFont,
8 | 'Segoe UI',
9 | Roboto,
10 | Oxygen,
11 | Ubuntu,
12 | Cantarell,
13 | 'Open Sans',
14 | 'Helvetica Neue',
15 | sans-serif;
16 | font-size: 16px;
17 |
18 | box-sizing: border-box;
19 | }
20 |
--------------------------------------------------------------------------------
/src/helpers/styles/variables.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --placeholder: black;
3 | }
4 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | import { sequence } from '@sveltejs/kit/hooks';
2 | import type { Handle } from '@sveltejs/kit';
3 |
4 | import { isLocale } from '$i18n/i18n-util';
5 |
6 | const language: Handle = async ({ event, resolve }) => {
7 | let [, lang] = event.url.pathname.split('/');
8 |
9 | if (isLocale(lang)) {
10 | // Set lang attribute
11 | return await resolve(event, {
12 | transformPageChunk: ({ html }) => html.replace('%lang%', lang)
13 | });
14 | }
15 |
16 | return await resolve(event);
17 | };
18 |
19 | export const handle: Handle = sequence(language);
20 |
--------------------------------------------------------------------------------
/src/i18n/de/index.ts:
--------------------------------------------------------------------------------
1 | import type { Translation } from '../i18n-types.js';
2 |
3 | const de: Translation = {
4 | // this is an example Translation, just rename or delete this folder if you want
5 | HI: 'Hallo {name}! Bitte hinterlasse einen Stern, wenn dir das Projekt gefällt: https://github.com/ivanhofer/typesafe-i18n',
6 | story: {
7 | button: '{times} mal geklickt'
8 | }
9 | };
10 |
11 | export default de;
12 |
--------------------------------------------------------------------------------
/src/i18n/en/index.ts:
--------------------------------------------------------------------------------
1 | import type { BaseTranslation } from '../i18n-types.js';
2 |
3 | const en: BaseTranslation = {
4 | // TODO: your translations go here
5 | HI: 'Hi {name:string}! Please leave a star if you like this project: https://github.com/ivanhofer/typesafe-i18n',
6 | story: {
7 | button: 'Clicked {times:number} times'
8 | }
9 | };
10 |
11 | export default en;
12 |
--------------------------------------------------------------------------------
/src/i18n/formatters.ts:
--------------------------------------------------------------------------------
1 | import type { FormattersInitializer } from 'typesafe-i18n'
2 | import type { Locales, Formatters } from './i18n-types.js'
3 |
4 | export const initFormatters: FormattersInitializer = (locale: Locales) => {
5 |
6 | const formatters: Formatters = {
7 | // add your formatter functions here
8 | }
9 |
10 | return formatters
11 | }
12 |
--------------------------------------------------------------------------------
/src/i18n/i18n-svelte.ts:
--------------------------------------------------------------------------------
1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
2 | /* eslint-disable */
3 |
4 | import { initI18nSvelte } from 'typesafe-i18n/svelte'
5 | import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types.js'
6 | import { loadedFormatters, loadedLocales } from './i18n-util.js'
7 |
8 | const { locale, LL, setLocale } = initI18nSvelte(loadedLocales, loadedFormatters)
9 |
10 | export { locale, LL, setLocale }
11 |
12 | export default LL
13 |
--------------------------------------------------------------------------------
/src/i18n/i18n-types.ts:
--------------------------------------------------------------------------------
1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
2 | /* eslint-disable */
3 | import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n'
4 |
5 | export type BaseTranslation = BaseTranslationType
6 | export type BaseLocale = 'en'
7 |
8 | export type Locales =
9 | | 'de'
10 | | 'en'
11 |
12 | export type Translation = RootTranslation
13 |
14 | export type Translations = RootTranslation
15 |
16 | type RootTranslation = {
17 | /**
18 | * Hi {name}! Please leave a star if you like this project: https://github.com/ivanhofer/typesafe-i18n
19 | * @param {string} name
20 | */
21 | HI: RequiredParams<'name'>
22 | story: {
23 | /**
24 | * Clicked {times} times
25 | * @param {number} times
26 | */
27 | button: RequiredParams<'times'>
28 | }
29 | }
30 |
31 | export type TranslationFunctions = {
32 | /**
33 | * Hi {name}! Please leave a star if you like this project: https://github.com/ivanhofer/typesafe-i18n
34 | */
35 | HI: (arg: { name: string }) => LocalizedString
36 | story: {
37 | /**
38 | * Clicked {times} times
39 | */
40 | button: (arg: { times: number }) => LocalizedString
41 | }
42 | }
43 |
44 | export type Formatters = {}
45 |
--------------------------------------------------------------------------------
/src/i18n/i18n-util.async.ts:
--------------------------------------------------------------------------------
1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
2 | /* eslint-disable */
3 |
4 | import { initFormatters } from './formatters.js'
5 | import type { Locales, Translations } from './i18n-types.js'
6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util.js'
7 |
8 | const localeTranslationLoaders = {
9 | de: () => import('./de/index.js'),
10 | en: () => import('./en/index.js'),
11 | }
12 |
13 | const updateDictionary = (locale: Locales, dictionary: Partial): Translations =>
14 | loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
15 |
16 | export const importLocaleAsync = async (locale: Locales): Promise =>
17 | (await localeTranslationLoaders[locale]()).default as unknown as Translations
18 |
19 | export const loadLocaleAsync = async (locale: Locales): Promise => {
20 | updateDictionary(locale, await importLocaleAsync(locale))
21 | loadFormatters(locale)
22 | }
23 |
24 | export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync))
25 |
26 | export const loadFormatters = (locale: Locales): void =>
27 | void (loadedFormatters[locale] = initFormatters(locale))
28 |
--------------------------------------------------------------------------------
/src/i18n/i18n-util.sync.ts:
--------------------------------------------------------------------------------
1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
2 | /* eslint-disable */
3 |
4 | import { initFormatters } from './formatters.js'
5 | import type { Locales, Translations } from './i18n-types.js'
6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util.js'
7 |
8 | import de from './de/index.js'
9 | import en from './en/index.js'
10 |
11 | const localeTranslations = {
12 | de,
13 | en,
14 | }
15 |
16 | export const loadLocale = (locale: Locales): void => {
17 | if (loadedLocales[locale]) return
18 |
19 | loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
20 | loadFormatters(locale)
21 | }
22 |
23 | export const loadAllLocales = (): void => locales.forEach(loadLocale)
24 |
25 | export const loadFormatters = (locale: Locales): void =>
26 | void (loadedFormatters[locale] = initFormatters(locale))
27 |
--------------------------------------------------------------------------------
/src/i18n/i18n-util.ts:
--------------------------------------------------------------------------------
1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
2 | /* eslint-disable */
3 |
4 | import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
5 | import type { LocaleDetector } from 'typesafe-i18n/detectors'
6 | import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n'
7 | import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
8 | import { initExtendDictionary } from 'typesafe-i18n/utils'
9 | import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types.js'
10 |
11 | export const baseLocale: Locales = 'en'
12 |
13 | export const locales: Locales[] = [
14 | 'de',
15 | 'en'
16 | ]
17 |
18 | export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales)
19 |
20 | export const loadedLocales: Record = {} as Record
21 |
22 | export const loadedFormatters: Record = {} as Record
23 |
24 | export const extendDictionary = initExtendDictionary()
25 |
26 | export const i18nString = (locale: Locales): TranslateByString => initI18nString(locale, loadedFormatters[locale])
27 |
28 | export const i18nObject = (locale: Locales): TranslationFunctions =>
29 | initI18nObject(
30 | locale,
31 | loadedLocales[locale],
32 | loadedFormatters[locale]
33 | )
34 |
35 | export const i18n = (): LocaleTranslationFunctions =>
36 | initI18n(loadedLocales, loadedFormatters)
37 |
38 | export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn(baseLocale, locales, ...detectors)
39 |
--------------------------------------------------------------------------------
/src/lib/components/_reusables/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/ad2e43a8917414d4800158b959e32b808522b649/src/lib/components/_reusables/.gitkeep
--------------------------------------------------------------------------------
/src/lib/components/atoms/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/ad2e43a8917414d4800158b959e32b808522b649/src/lib/components/atoms/.gitkeep
--------------------------------------------------------------------------------
/src/lib/components/atoms/Button/Button.stories.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/atoms/Button/Button.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/atoms/Logo/Logo.stories.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/lib/components/atoms/Logo/Logo.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/components/atoms/Logo/svelte.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/components/molecules/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/ad2e43a8917414d4800158b959e32b808522b649/src/lib/components/molecules/.gitkeep
--------------------------------------------------------------------------------
/src/lib/components/organisms/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/ad2e43a8917414d4800158b959e32b808522b649/src/lib/components/organisms/.gitkeep
--------------------------------------------------------------------------------
/src/lib/server/prisma.ts:
--------------------------------------------------------------------------------
1 | import { env } from '$env/dynamic/private';
2 | import { PrismaClient } from '@prisma/client';
3 |
4 | export const prisma = new PrismaClient({
5 | datasources: {
6 | db: {
7 | url: env.DATABASE_URL
8 | }
9 | }
10 | });
11 |
--------------------------------------------------------------------------------
/src/lib/server/trpc/_app.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from '$lib/server/prisma';
2 | import { z } from 'zod';
3 | import { protectedProcedure, publicProcedure, router } from './server';
4 |
5 | export const appRouter = router({
6 | substraction: publicProcedure
7 | .input(z.object({ minuend: z.number(), subtrahend: z.number() }))
8 | .query(({ input }) => input.minuend - input.subtrahend),
9 | pong: protectedProcedure.query(() => 'ping'),
10 | users: router({
11 | count: publicProcedure.query(async () => await prisma.user.count())
12 | })
13 | });
14 |
15 | export type AppRouter = typeof appRouter;
16 |
--------------------------------------------------------------------------------
/src/lib/server/trpc/createContext.ts:
--------------------------------------------------------------------------------
1 | import type { RequestEvent } from '@sveltejs/kit';
2 | import type { inferAsyncReturnType } from '@trpc/server';
3 |
4 | export const createContext = async (event: RequestEvent) => {
5 | return {};
6 | };
7 |
8 | export type Context = inferAsyncReturnType;
9 |
--------------------------------------------------------------------------------
/src/lib/server/trpc/middleware/isAuthenticated.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from '@trpc/server';
2 | import type { t } from '$lib/server/trpc/server';
3 |
4 | export const isAuthenticated = ($t: typeof t) =>
5 | $t.middleware(({ next, ctx }) => {
6 | if (true) {
7 | throw new TRPCError({ code: 'UNAUTHORIZED' });
8 | }
9 |
10 | return next({
11 | ctx: {
12 | session: null
13 | }
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/lib/server/trpc/server.ts:
--------------------------------------------------------------------------------
1 | import { initTRPC } from '@trpc/server';
2 | import type { Context } from '$lib/server/trpc/createContext';
3 | import { isAuthenticated } from '$lib/server/trpc/middleware/isAuthenticated';
4 |
5 | export const t = initTRPC.context().create({
6 | errorFormatter({ shape, error }) {
7 | console.log(error);
8 | return shape;
9 | }
10 | });
11 |
12 | export const router = t.router;
13 | export const middleware = t.middleware;
14 |
15 | export const publicProcedure = t.procedure;
16 | export const protectedProcedure = t.procedure.use(isAuthenticated(t));
17 |
--------------------------------------------------------------------------------
/src/routes/(api)/trpc/[...args]/+server.ts:
--------------------------------------------------------------------------------
1 | import type { RequestEvent } from '@sveltejs/kit';
2 | import type { AnyRouter, Dict } from '@trpc/server';
3 | import type { RequestHandler } from './$types';
4 |
5 | import { createContext } from '$lib/server/trpc/createContext';
6 | import { appRouter } from '$lib/server/trpc/_app';
7 | import { resolveHTTPResponse } from '@trpc/server/http';
8 |
9 | async function handler(
10 | event: RequestEvent,
11 | router: AnyRouter,
12 | createContext: any,
13 | responseMeta?: any,
14 | onError?: any
15 | ) {
16 | const request = event.request as Request & {
17 | headers: Dict;
18 | };
19 |
20 | const req = {
21 | method: request.method,
22 | headers: request.headers,
23 | query: event.url.searchParams,
24 | body: await request.text()
25 | };
26 |
27 | const httpResponse = await resolveHTTPResponse({
28 | router,
29 | req,
30 | path: event.url.pathname.substring('/trpc'.length + 1),
31 | createContext: async () => createContext?.(event),
32 | responseMeta,
33 | onError
34 | });
35 |
36 | const { status, headers, body } = httpResponse as {
37 | status: number;
38 | headers: Record;
39 | body: string;
40 | };
41 |
42 | return new Response(body, { status, headers });
43 | }
44 |
45 | export const GET: RequestHandler = async (event) => {
46 | return await handler(event, appRouter, createContext);
47 | };
48 |
49 | export const POST: RequestHandler = async (event) => {
50 | return await handler(event, appRouter, createContext);
51 | };
52 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutServerLoad } from './$types';
2 |
3 | import { redirect } from '@sveltejs/kit';
4 | import parser from 'accept-language-parser';
5 |
6 | import { baseLocale, locales } from '$i18n/i18n-util';
7 | import type { Locales } from '$i18n/i18n-types';
8 |
9 | export const load: LayoutServerLoad = async (event) => {
10 | const browserLanguage = parser.pick(locales, event.request.headers.get('accept-language') || '');
11 | const locale = event.params.locale as Locales;
12 |
13 | if (!locale || !locales.includes(locale)) {
14 | let pathname = event.url.pathname.split('/').filter(Boolean);
15 | pathname.unshift((browserLanguage as string) || baseLocale);
16 |
17 | redirect(308, pathname.join('/'));
18 | }
19 |
20 | return {
21 | locale
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/routes/(app)/+page.svelte:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/ad2e43a8917414d4800158b959e32b808522b649/src/routes/(app)/+page.svelte
--------------------------------------------------------------------------------
/src/routes/(app)/[locale]/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { PageServerLoad } from './$types';
2 |
3 | import { client } from '$scripts/trpc';
4 |
5 | export const load: PageServerLoad = async (event) => {
6 | return {
7 | userCount: await client(event).users.count.query()
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/src/routes/(app)/[locale]/+page.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 | {$LL.HI({ name: 'NULL' })}
23 |
24 |
25 |
26 | {data.userCount} user(-s)
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/ad2e43a8917414d4800158b959e32b808522b649/static/favicon.png
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import { SassAlias } from 'svelte-preprocess-sass-alias-import';
2 | import adapter from '@sveltejs/adapter-node';
3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
4 | import path from 'path';
5 |
6 | export const alias = new SassAlias({
7 | $styles: ['src', 'helpers', 'styles']
8 | });
9 |
10 | /** @type {import('@sveltejs/kit').Config} */
11 | const config = {
12 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
13 | // for more information about preprocessors
14 | preprocess: vitePreprocess({
15 | sass: {
16 | importer: [alias.resolve.bind(alias)]
17 | },
18 | scss: {
19 | importer: [alias.resolve.bind(alias)]
20 | }
21 | }),
22 |
23 | kit: {
24 | adapter: adapter(),
25 | alias: {
26 | $assets: path.join('src', 'assets'),
27 | $scripts: path.join('src', 'helpers', 'scripts'),
28 | $styles: path.join('src', 'helpers', 'styles'),
29 | $i18n: path.join('src', 'i18n'),
30 | $lib: path.join('src', 'lib'),
31 | $atoms: path.join('src', 'lib', 'components', 'atoms'),
32 | $molecules: path.join('src', 'lib', 'components', 'molecules'),
33 | $organisms: path.join('src', 'lib', 'components', 'organisms'),
34 | $components: path.join('src', 'lib', 'components')
35 | }
36 | }
37 | };
38 |
39 | export default config;
40 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | },
13 | "include": [
14 | "./.svelte-kit/ambient.d.ts",
15 | "./.svelte-kit/types/**/$types.d.ts",
16 | "./vite.config.ts",
17 | "./src/**/*.js",
18 | "./src/**/*.ts",
19 | "./src/**/*.svelte",
20 | "./src/**/*.js",
21 | "./src/**/*.ts",
22 | "./src/**/*.svelte",
23 | "./tests/**/*.js",
24 | "./tests/**/*.ts",
25 | "./tests/**/*.svelte",
26 | "./.storybook/**/*.svelte",
27 | "./.storybook/**/*.ts",
28 | "./types/**/*.d.ts",
29 | "./prisma/seed.ts"
30 | ]
31 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
32 | //
33 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
34 | // from the referenced tsconfig.json - TypeScript does not merge them in
35 | }
36 |
--------------------------------------------------------------------------------
/types/HTMLImageElement.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace svelte.JSX {
2 | interface IHTMLImageElement extends HTMLProps, HTMLAttributes {
3 | fetchpriority: 'high' | 'low';
4 | }
5 |
6 | interface IntrinsicElements {
7 | img: IHTMLImageElement;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/types/poppanator-sveltekit-svg.d.ts:
--------------------------------------------------------------------------------
1 | import '@poppanator/sveltekit-svg/dist/svg';
--------------------------------------------------------------------------------
/types/sveltejs-enhanced-img.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*?enhanced' {
2 | const value: String;
3 | export default value;
4 | }
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import svg from '@poppanator/sveltekit-svg';
3 | import { enhancedImages } from '@sveltejs/enhanced-img';
4 |
5 | import { alias } from './svelte.config';
6 |
7 | /** @type {import('vite').UserConfig} */
8 | const config = {
9 | plugins: [
10 | sveltekit(),
11 | svg(),
12 | enhancedImages()
13 | ],
14 | css: {
15 | preprocessorOptions: {
16 | sass: {
17 | importer: [alias.resolve.bind(alias)]
18 | },
19 | scss: {
20 | importer: [alias.resolve.bind(alias)]
21 | }
22 | }
23 | }
24 | };
25 |
26 | export default config;
27 |
--------------------------------------------------------------------------------