├── src ├── routes │ ├── __layout.svelte │ ├── posts │ │ ├── [slug].ts │ │ ├── new.ts │ │ ├── [slug].svelte │ │ └── new.svelte │ ├── auth │ │ ├── login.ts │ │ ├── register.ts │ │ ├── __layout.svelte │ │ ├── login.svelte │ │ └── register.svelte │ ├── index.ts │ └── index.svelte ├── app.css ├── lib │ ├── db.ts │ ├── post.ts │ └── auth.ts ├── hooks.ts ├── app.html └── app.d.ts ├── static └── favicon.png ├── .prettierrc ├── .gitignore ├── tailwind.config.cjs ├── svemix.config.js ├── postcss.config.cjs ├── svelte.config.js ├── tsconfig.json ├── prisma ├── schema.prisma └── seed.ts ├── README.md ├── package.json └── pnpm-lock.yaml /src/routes/__layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixelmund/svemix-example/HEAD/static/favicon.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | dev.db 9 | .db 10 | /dev.db -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in PostCSS syntax */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import Prisma, * as PrismaAll from "@prisma/client"; 2 | 3 | const PrismaClient = Prisma?.PrismaClient || PrismaAll?.PrismaClient; 4 | export default new PrismaClient({log: ['error', 'info', 'query', 'warn']}); -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | content: ['./src/**/*.{html,js,svelte,ts}'], 3 | 4 | theme: { 5 | extend: {} 6 | }, 7 | 8 | plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')] 9 | }; 10 | 11 | module.exports = config; 12 | -------------------------------------------------------------------------------- /svemix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('vite-plugin-svemix').SvemixConfig} */ 2 | const config = { 3 | seo: { 4 | title: 'Svemix Example', 5 | description: 'This is the svemix blog example', 6 | keywords: 'svemix,example,blog' 7 | } 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { GetSession } from '@sveltejs/kit'; 2 | import { handleSession } from 'svemix/session'; 3 | 4 | export const getSession: GetSession = ({ locals }) => { 5 | return locals.session.data; 6 | }; 7 | 8 | export const handle = handleSession({ secret: 'SOME_SECRET_VALUE', getSession }); 9 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %svelte.head% 8 | 9 | 10 |
%svelte.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface SessionData { 4 | isLoggedIn: boolean; 5 | user: Pick 6 | } 7 | 8 | // See https://kit.svelte.dev/docs#typescript 9 | // for information about these interfaces 10 | declare namespace App { 11 | interface Locals { 12 | session: import('svemix/session').Session; 13 | } 14 | 15 | interface Platform {} 16 | 17 | interface Session extends SessionData {} 18 | 19 | interface Stuff {} 20 | } 21 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | const cssnano = require('cssnano'); 4 | 5 | const mode = process.env.NODE_ENV; 6 | const dev = mode === 'development'; 7 | 8 | const config = { 9 | plugins: [ 10 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 11 | tailwindcss(), 12 | //But others, like autoprefixer, need to run after, 13 | autoprefixer(), 14 | !dev && 15 | cssnano({ 16 | preset: 'default' 17 | }) 18 | ] 19 | }; 20 | 21 | module.exports = config; 22 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import preprocess from 'svelte-preprocess'; 3 | import svemix from 'svemix/plugin'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://github.com/sveltejs/svelte-preprocess 8 | // for more information about preprocessors 9 | preprocess: [ 10 | preprocess({ 11 | postcss: true 12 | }) 13 | ], 14 | 15 | kit: { 16 | adapter: adapter(), 17 | vite: { 18 | plugins: [ 19 | svemix() 20 | ] 21 | } 22 | } 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /src/routes/posts/[slug].ts: -------------------------------------------------------------------------------- 1 | import { get as __get, post as __post } from 'svemix/server'; 2 | 3 | import type { Loader } from 'svemix/server'; 4 | import type { Post } from '@prisma/client'; 5 | import db from '$lib/db'; 6 | 7 | interface LoaderData { 8 | post: Post; 9 | } 10 | 11 | export const loader: Loader = async function ({ params }) { 12 | try { 13 | const post = await db.post.findUnique({ 14 | where: { slug: params.slug }, 15 | rejectOnNotFound: false 16 | }); 17 | 18 | if (!post) { 19 | throw new Error('Post not found'); 20 | } 21 | 22 | return { 23 | post, 24 | metadata: { 25 | title: post.title, 26 | description: post.excerpt 27 | } 28 | }; 29 | } catch (error) { 30 | throw new Error(error); 31 | } 32 | }; 33 | 34 | export const get = __get(loader); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2020", 5 | "lib": ["es2020", "DOM"], 6 | "target": "es2020", 7 | /** 8 | svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 9 | to enforce using \`import type\` instead of \`import\` for Types. 10 | */ 11 | "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | To have warnings/errors of the Svelte compiler at the correct position, 16 | enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "baseUrl": ".", 23 | "allowJs": true, 24 | "checkJs": true, 25 | "paths": { 26 | "$lib": ["src/lib"], 27 | "$lib/*": ["src/lib/*"] 28 | } 29 | }, 30 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] 31 | } 32 | -------------------------------------------------------------------------------- /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 = "file:./dev.db" 11 | } 12 | 13 | model User { 14 | id String @id @default(uuid()) 15 | username String @unique 16 | email String @unique 17 | passwordHash Bytes 18 | 19 | posts Post[] 20 | 21 | createdAt DateTime @default(now()) 22 | updatedAt DateTime @updatedAt 23 | } 24 | 25 | model Post { 26 | id String @id @default(uuid()) 27 | 28 | title String 29 | slug String @unique 30 | content String 31 | featured_image String? 32 | read_time Int? @default(5) 33 | excerpt String @default("") 34 | 35 | user User @relation(fields: [userId], references: [id]) 36 | userId String 37 | 38 | createdAt DateTime @default(now()) 39 | updatedAt DateTime @updatedAt 40 | } 41 | -------------------------------------------------------------------------------- /src/routes/auth/login.ts: -------------------------------------------------------------------------------- 1 | import { get as __get, post as __post } from 'svemix/server'; 2 | 3 | import { authenticateUser } from '$lib/auth'; 4 | import type { Action } from 'svemix/server'; 5 | 6 | export const action: Action = async function ({ request, locals }) { 7 | const body = await request.formData(); 8 | 9 | const email = body.get('email') as string; 10 | const password = body.get('password') as string; 11 | 12 | try { 13 | const { user, errors } = await authenticateUser(email, password); 14 | 15 | if (errors.email || errors.password) { 16 | return { 17 | values: { 18 | email, 19 | password 20 | }, 21 | errors: { 22 | email: errors.email, 23 | password: errors.password 24 | } 25 | }; 26 | } 27 | 28 | delete user?.passwordHash; 29 | 30 | locals.session.data = { isLoggedIn: true, user }; 31 | 32 | return { 33 | values: { 34 | email, 35 | password 36 | } 37 | }; 38 | } catch (error) { 39 | return { 40 | values: { 41 | email, 42 | password 43 | }, 44 | formError: error.message 45 | }; 46 | } 47 | }; 48 | 49 | export const get = __get(() => ({})); 50 | export const post = __post(action); 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte); 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm init svelte@next 12 | 13 | # create a new project in my-app 14 | npm init svelte@next my-app 15 | ``` 16 | 17 | > Note: the `@next` is temporary 18 | 19 | ## Developing 20 | 21 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 22 | 23 | ```bash 24 | npm run dev 25 | 26 | # or start the server and open the app in a new browser tab 27 | npm run dev -- --open 28 | ``` 29 | 30 | ## Building 31 | 32 | Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then: 33 | 34 | ```bash 35 | npm run build 36 | ``` 37 | 38 | > You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production. 39 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { get as __get, post as __post } from 'svemix/server'; 2 | 3 | import type { Action, Loader } from 'svemix/server'; 4 | import type { Post } from '@prisma/client'; 5 | import db from '$lib/db'; 6 | 7 | interface LoaderData { 8 | posts: Post[]; 9 | pageInfo: { 10 | totalPages: number; 11 | currentPage: number; 12 | totalCount: number; 13 | }; 14 | } 15 | 16 | const TAKE = 6; 17 | 18 | export const loader: Loader = async function ({ url }) { 19 | const page = parseInt(url.searchParams.get('page')) || 1; 20 | const skip = (page - 1) * TAKE; 21 | 22 | const [totalCount, posts] = await db.$transaction([ 23 | db.post.count(), 24 | db.post.findMany({ take: TAKE, skip, orderBy: { createdAt: 'desc' } }) 25 | ]); 26 | 27 | const totalPages = Math.ceil(totalCount / TAKE); 28 | 29 | return { 30 | pageInfo: { 31 | totalPages, 32 | currentPage: page, 33 | totalCount 34 | }, 35 | posts 36 | }; 37 | }; 38 | 39 | export const action: Action = async function ({ locals, request }) { 40 | const body = await request.formData(); 41 | 42 | const _action = body.get('_action'); 43 | 44 | if (_action === 'logout') { 45 | locals.session.destroy(); 46 | } 47 | 48 | return {}; 49 | }; 50 | 51 | export const get = __get(loader); 52 | export const post = __post(action); 53 | -------------------------------------------------------------------------------- /src/routes/posts/new.ts: -------------------------------------------------------------------------------- 1 | import { get as __get, post as __post } from 'svemix/server'; 2 | 3 | import { Action, Loader, redirect } from 'svemix/server'; 4 | import { readingTime, truncateString, slugify } from '$lib/post'; 5 | import db from '$lib/db'; 6 | 7 | interface LoadedData {} 8 | 9 | interface ActionData { 10 | title: string; 11 | content: string; 12 | } 13 | 14 | export const action: Action = async ({ request, locals }) => { 15 | const body = await request.formData(); 16 | 17 | const title = body.get('title') as string; 18 | const content = body.get('content') as string; 19 | 20 | if (!title || title.length === 0) { 21 | return { 22 | errors: { 23 | content: '', 24 | title: 'Please provide a title' 25 | } 26 | }; 27 | } 28 | 29 | const slug = slugify(title); 30 | const excerpt = truncateString(content, 150); 31 | const readTime = readingTime(content); 32 | 33 | const post = await db.post.create({ 34 | data: { 35 | content, 36 | slug, 37 | read_time: readTime, 38 | excerpt, 39 | title, 40 | user: { connect: { id: locals.session.data.user.id } } 41 | } 42 | }); 43 | 44 | return redirect(`/posts/${post.slug}`, 302); 45 | }; 46 | 47 | export const loader: Loader = ({ locals }) => { 48 | if (!locals.session.data?.isLoggedIn) { 49 | return redirect('/auth/login', 302); 50 | } 51 | 52 | return {}; 53 | }; 54 | 55 | export const get = __get(loader); 56 | export const post = __post(action); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svemix-example", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "svelte-kit dev", 6 | "build": "svelte-kit build", 7 | "package": "svelte-kit package", 8 | "preview": "svelte-kit preview", 9 | "check": "svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. .", 12 | "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. .", 13 | "postinstall": "npx prisma db push && npx prisma db seed" 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/adapter-node": "^1.0.0-next.58", 17 | "@sveltejs/kit": "next", 18 | "@tailwindcss/forms": "^0.4.0", 19 | "@tailwindcss/typography": "^0.5.0", 20 | "@types/node": "^17.0.5", 21 | "autoprefixer": "^10.4.1", 22 | "cssnano": "^5.0.14", 23 | "postcss": "^8.4.5", 24 | "postcss-load-config": "^3.1.0", 25 | "prettier": "^2.5.1", 26 | "prettier-plugin-svelte": "^2.5.1", 27 | "prisma": "^3.9.1", 28 | "svelte": "^3.44.3", 29 | "svelte-check": "^2.2.11", 30 | "svelte-preprocess": "^4.10.1", 31 | "tailwindcss": "^3.0.8", 32 | "ts-node": "^10.4.0", 33 | "tslib": "^2.3.1", 34 | "typescript": "^4.5.4" 35 | }, 36 | "type": "module", 37 | "dependencies": { 38 | "@prisma/client": "^3.9.1", 39 | "@types/secure-password": "^3.1.1", 40 | "secure-password": "^4.0.0", 41 | "svemix": "^0.8.2" 42 | }, 43 | "prisma": { 44 | "seed": "node --loader ts-node/esm ./prisma/seed.ts" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/routes/auth/register.ts: -------------------------------------------------------------------------------- 1 | import { get as __get, post as __post } from 'svemix/server'; 2 | 3 | import { hashPassword } from '$lib/auth'; 4 | import type { Action } from 'svemix/server'; 5 | import db from '$lib/db'; 6 | 7 | interface ActionData { 8 | username?: string; 9 | email?: string; 10 | password?: string; 11 | } 12 | 13 | export const action: Action = async function ({ request, locals }) { 14 | const body = await request.formData(); 15 | 16 | const username = body.get('username') as string; 17 | const email = body.get('email') as string; 18 | const password = body.get('password') as string; 19 | 20 | const user = await db.user.findUnique({ where: { email } }); 21 | 22 | if (user) { 23 | return { 24 | values: { 25 | username, 26 | email, 27 | password 28 | }, 29 | errors: { 30 | email: 'User with given E-Mail already exists', 31 | password: '', 32 | username: '' 33 | } 34 | }; 35 | } 36 | 37 | const passwordHash = await hashPassword(password); 38 | 39 | try { 40 | const newUser = await db.user.create({ 41 | data: { 42 | email, 43 | passwordHash, 44 | username 45 | } 46 | }); 47 | 48 | delete newUser?.passwordHash; 49 | 50 | locals.session.data = { isLoggedIn: true, user: newUser }; 51 | 52 | return {}; 53 | } catch (error) { 54 | return { 55 | values: { 56 | username, 57 | email, 58 | password 59 | }, 60 | errors: { 61 | username: 'Username already exists', 62 | email: '', 63 | password: '' 64 | } 65 | }; 66 | } 67 | }; 68 | 69 | export const get = __get(() => ({})); 70 | export const post = __post(action); 71 | -------------------------------------------------------------------------------- /src/lib/post.ts: -------------------------------------------------------------------------------- 1 | export function truncateString(str: string, length: number) { 2 | if (str.length > length) { 3 | return str.slice(0, length) + '...'; 4 | } else { 5 | return str; 6 | } 7 | } 8 | 9 | export function readingTime(content: string) { 10 | const wpm = 225; 11 | const words = content.trim().split(/\s+/).length; 12 | const time = Math.ceil(words / wpm); 13 | return time; 14 | } 15 | 16 | export const slugify = (text: string) => { 17 | // Use hash map for special characters 18 | let specialChars = { 19 | à: 'a', 20 | ä: 'a', 21 | á: 'a', 22 | â: 'a', 23 | æ: 'a', 24 | å: 'a', 25 | ë: 'e', 26 | è: 'e', 27 | é: 'e', 28 | ê: 'e', 29 | î: 'i', 30 | ï: 'i', 31 | ì: 'i', 32 | í: 'i', 33 | ò: 'o', 34 | ó: 'o', 35 | ö: 'o', 36 | ô: 'o', 37 | ø: 'o', 38 | ù: 'o', 39 | ú: 'u', 40 | ü: 'u', 41 | û: 'u', 42 | ñ: 'n', 43 | ç: 'c', 44 | ß: 's', 45 | ÿ: 'y', 46 | œ: 'o', 47 | ŕ: 'r', 48 | ś: 's', 49 | ń: 'n', 50 | ṕ: 'p', 51 | ẃ: 'w', 52 | ǵ: 'g', 53 | ǹ: 'n', 54 | ḿ: 'm', 55 | ǘ: 'u', 56 | ẍ: 'x', 57 | ź: 'z', 58 | ḧ: 'h', 59 | '·': '-', 60 | '/': '-', 61 | _: '-', 62 | ',': '-', 63 | ':': '-', 64 | ';': '-' 65 | }; 66 | 67 | return text 68 | .toString() 69 | .toLowerCase() 70 | .replace(/\s+/g, '-') // Replace spaces with - 71 | .replace(/./g, (target, index, str) => specialChars[target] || target) // Replace special characters using the hash map 72 | .replace(/&/g, '-and-') // Replace & with 'and' 73 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 74 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 75 | .replace(/^-+/, '') // Trim - from start of text 76 | .replace(/-+$/, ''); // Trim - from end of text 77 | }; 78 | -------------------------------------------------------------------------------- /src/routes/posts/[slug].svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | 39 | 40 |
41 |
42 |
43 |

44 | Article 48 | 51 | {$data.post.title} 52 | 53 |

54 |

55 | {$data.post.content} 56 |

57 | View all posts 61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /src/routes/auth/__layout.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 42 | 43 |
44 |
45 |

46 | {signIn ? 'Sign in' : 'Sign up'} 47 |

48 |

49 | Or 50 | 54 | {signIn ? 'create an account' : 'log in to your account.'} 55 | 56 |

57 |
58 |
59 |
63 | 64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import db from '$lib/db'; 2 | import SecurePassword from 'secure-password'; 3 | 4 | const securePassword = new SecurePassword(); 5 | 6 | /** 7 | * Hash a plain text password and return the hashed password. 8 | */ 9 | export async function hashPassword(password: string) { 10 | return await securePassword.hash(Buffer.from(password)); 11 | } 12 | 13 | /** 14 | * Verify that a hashed password and a plain text password match. 15 | */ 16 | export async function verifyPassword(hashedPassword: Buffer, password: string) { 17 | try { 18 | return await securePassword.verify(Buffer.from(password), hashedPassword); 19 | } catch (error) { 20 | console.error(error); 21 | return SecurePassword.INVALID; 22 | } 23 | } 24 | 25 | export function passwordIsValid(validity: symbol) { 26 | return [SecurePassword.VALID, SecurePassword.VALID_NEEDS_REHASH].includes(validity); 27 | } 28 | 29 | /** 30 | * Attempts to authenticate a user, given their username and password. 31 | */ 32 | export async function authenticateUser(email: string, password: string, query?: any) { 33 | const user = await db.user.findFirst({ 34 | ...(query ? query : {}), 35 | where: { 36 | email: { 37 | equals: email 38 | } 39 | } 40 | }); 41 | 42 | if (!user || !user.passwordHash) { 43 | return { 44 | user: null, 45 | errors: { 46 | email: 'Email not found', 47 | password: null 48 | } 49 | }; 50 | } 51 | 52 | const passwordStatus = await verifyPassword(user.passwordHash, password); 53 | 54 | switch (passwordStatus) { 55 | case SecurePassword.VALID: 56 | break; 57 | case SecurePassword.VALID_NEEDS_REHASH: 58 | // If the password was hashed with a less-secure hash, we will seamlessly 59 | // upgrade it to the more secure version. 60 | const improvedHash = await hashPassword(password); 61 | await db.user.update({ 62 | where: { id: user.id }, 63 | data: { passwordHash: improvedHash } 64 | }); 65 | break; 66 | default: 67 | return { 68 | user: null, 69 | errors: { 70 | email: null, 71 | password: 'Password is incorrect.' 72 | } 73 | }; 74 | } 75 | 76 | return { 77 | user, 78 | errors: {} 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import Prisma from '@prisma/client'; 2 | 3 | import SecurePassword from 'secure-password'; 4 | const securePassword = new SecurePassword(); 5 | 6 | /** 7 | * Hash a plain text password and return the hashed password. 8 | */ 9 | export async function hashPassword(password: string) { 10 | return await securePassword.hash(Buffer.from(password)); 11 | } 12 | 13 | function truncateString(str: string, length: number) { 14 | if (str.length > length) { 15 | return str.slice(0, length) + '...'; 16 | } else { 17 | return str; 18 | } 19 | } 20 | 21 | function readingTime(content: string) { 22 | const wpm = 225; 23 | const words = content.trim().split(/\s+/).length; 24 | const time = Math.ceil(words / wpm); 25 | return time; 26 | } 27 | 28 | const db = new Prisma.PrismaClient({ 29 | log: ['warn', 'info', 'query', 'error'] 30 | }); 31 | 32 | async function main() { 33 | const user1 = await db.user.upsert({ 34 | where: { 35 | email: 'user1@gmail.com' 36 | }, 37 | create: { 38 | email: 'user1@gmail.com', 39 | passwordHash: await hashPassword('123456'), 40 | username: 'user1' 41 | }, 42 | update: {} 43 | }); 44 | 45 | const content = 46 | 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.

Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'; 47 | const excerpt = truncateString(content, 150); 48 | const readTime = readingTime(content); 49 | 50 | const posts = [ 51 | db.post.upsert({ 52 | where: { 53 | slug: 'my-first-post' 54 | }, 55 | update: { 56 | }, 57 | create: { 58 | title: 'My first post', 59 | slug: 'my-first-post', 60 | user: { connect: { id: user1.id } }, 61 | content, 62 | read_time: readTime, 63 | excerpt 64 | } 65 | }), 66 | db.post.upsert({ 67 | where: { 68 | slug: 'my-second-post' 69 | }, 70 | update: { 71 | 72 | }, 73 | create: { 74 | title: 'My second post', 75 | slug: 'my-second-post', 76 | user: { connect: { id: user1.id } }, 77 | content, 78 | read_time: readTime, 79 | excerpt 80 | } 81 | }), 82 | db.post.upsert({ 83 | where: { 84 | slug: 'my-third-post' 85 | }, 86 | update: { 87 | }, 88 | create: { 89 | title: 'My third post', 90 | slug: 'my-third-post', 91 | user: { connect: { id: user1.id } }, 92 | content, 93 | read_time: readTime, 94 | excerpt 95 | } 96 | }) 97 | ]; 98 | 99 | await db.$transaction(posts); 100 | } 101 | 102 | main() 103 | .catch((e) => { 104 | console.error(e); 105 | process.exit(1); 106 | }) 107 | .finally(async () => { 108 | await db.$disconnect(); 109 | }); 110 | -------------------------------------------------------------------------------- /src/routes/posts/new.svelte: -------------------------------------------------------------------------------- 1 | 54 | 55 | 58 | 59 |
60 |
61 |

New Post

62 |
63 |
64 |
65 |
66 |
67 |
68 | 69 |
70 | 77 |
78 |
79 |
80 | 81 |
82 |