├── .npmrc ├── src ├── lib │ ├── components │ │ ├── atoms │ │ │ ├── .gitkeep │ │ │ ├── Logo │ │ │ │ ├── Logo.svelte │ │ │ │ ├── Logo.stories.svelte │ │ │ │ └── svelte.svg │ │ │ └── Button │ │ │ │ ├── Button.stories.svelte │ │ │ │ └── Button.svelte │ │ ├── _reusables │ │ │ └── .gitkeep │ │ ├── molecules │ │ │ └── .gitkeep │ │ └── organisms │ │ │ └── .gitkeep │ └── server │ │ ├── prisma.ts │ │ └── trpc │ │ ├── createContext.ts │ │ ├── middleware │ │ └── isAuthenticated.ts │ │ ├── server.ts │ │ └── _app.ts ├── routes │ ├── (app) │ │ ├── +page.svelte │ │ ├── [locale] │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── +layout.svelte │ │ └── +layout.server.ts │ └── (api) │ │ └── trpc │ │ └── [...args] │ │ └── +server.ts ├── helpers │ ├── styles │ │ ├── variables.scss │ │ ├── a11y.scss │ │ └── main.scss │ └── scripts │ │ └── trpc.ts ├── assets │ └── bigben.avif ├── app.d.ts ├── i18n │ ├── en │ │ └── index.ts │ ├── formatters.ts │ ├── de │ │ └── index.ts │ ├── i18n-svelte.ts │ ├── i18n-util.sync.ts │ ├── i18n-util.async.ts │ ├── i18n-types.ts │ └── i18n-util.ts ├── app.html └── hooks.server.ts ├── .storybook ├── preview-head.html ├── Wrapper.svelte ├── preview.ts └── main.ts ├── types ├── poppanator-sveltekit-svg.d.ts ├── sveltejs-enhanced-img.d.ts └── HTMLImageElement.d.ts ├── static └── favicon.png ├── .typesafe-i18n.json ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20221220114054_init │ │ └── migration.sql ├── schema.prisma └── seed.ts ├── docker-compose.yml ├── .gitignore ├── .prettierignore ├── .eslintignore ├── .prettierrc ├── postcss.config.cjs ├── .env.example ├── .eslintrc.cjs ├── Dockerfile ├── vite.config.js ├── tsconfig.json ├── LICENSE ├── README.md ├── svelte.config.js └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /src/lib/components/atoms/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/(app)/+page.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/components/_reusables/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/components/molecules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/components/organisms/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/helpers/styles/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --placeholder: black; 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types/poppanator-sveltekit-svg.d.ts: -------------------------------------------------------------------------------- 1 | import '@poppanator/sveltekit-svg/dist/svg'; -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/assets/bigben.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucagoslar/sveltekit-fullstack/HEAD/src/assets/bigben.avif -------------------------------------------------------------------------------- /types/sveltejs-enhanced-img.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*?enhanced' { 2 | const value: String; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /src/lib/components/atoms/Logo/Logo.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.storybook/Wrapper.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/components/atoms/Logo/Logo.stories.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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/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/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/routes/(app)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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" -------------------------------------------------------------------------------- /.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 | }; -------------------------------------------------------------------------------- /src/lib/components/atoms/Button/Button.stories.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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" ] -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/lib/components/atoms/Button/Button.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 29 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | * H​i​ ​{​n​a​m​e​}​!​ ​P​l​e​a​s​e​ ​l​e​a​v​e​ ​a​ ​s​t​a​r​ ​i​f​ ​y​o​u​ ​l​i​k​e​ ​t​h​i​s​ ​p​r​o​j​e​c​t​:​ ​h​t​t​p​s​:​/​/​g​i​t​h​u​b​.​c​o​m​/​i​v​a​n​h​o​f​e​r​/​t​y​p​e​s​a​f​e​-​i​1​8​n 19 | * @param {string} name 20 | */ 21 | HI: RequiredParams<'name'> 22 | story: { 23 | /** 24 | * C​l​i​c​k​e​d​ ​{​t​i​m​e​s​}​ ​t​i​m​e​s 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/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/lib/components/atoms/Logo/svelte.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 |