({
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 | * 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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------