├── .env ├── .github └── workflows │ ├── build.yml │ └── update.yml ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── migrations │ ├── 20230905040838_init │ │ └── migration.sql │ ├── 20241222160427_add_space_owner │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── schema.zmodel ├── src ├── app.css ├── app.d.ts ├── app.html ├── components │ └── List.svelte ├── hooks.server.ts ├── lib │ ├── auth.ts │ ├── components │ │ ├── Avatar.svelte │ │ ├── BreadCrumb.svelte │ │ ├── CreateListDialog.svelte │ │ ├── ManageMembers.svelte │ │ ├── NavBar.svelte │ │ ├── SpaceMembers.svelte │ │ ├── Spaces.svelte │ │ ├── TimeInfo.svelte │ │ ├── Todo.svelte │ │ └── TodoList.svelte │ ├── constant.ts │ ├── hooks │ │ ├── __model_meta.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── space-user.ts │ │ ├── space.ts │ │ ├── todo.ts │ │ └── user.ts │ └── prisma.ts └── routes │ ├── (app) │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +page.server.ts │ ├── +page.svelte │ ├── create-space │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── space │ │ └── [slug] │ │ ├── +layout.server.ts │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ └── [listId] │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── (auth) │ ├── signin │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── signup │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── +layout.svelte │ └── api │ └── auth │ ├── signin │ └── +server.ts │ └── signout │ └── +server.ts ├── static ├── auth-bg.jpg ├── avatar.jpg └── logo.png ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://postgres:abc123@localhost:5432/todo-sveltekit 2 | DATABASE_DIRECT_URL=postgres://postgres:abc123@localhost:5432/todo-sveltekit 3 | JWT_SECRET=secret -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | DO_NOT_TRACK: '1' 5 | 6 | on: 7 | push: 8 | branches: ['main'] 9 | pull_request: 10 | branches: ['main'] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js 20.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.x 22 | cache: 'npm' 23 | - run: npm ci 24 | - run: npm run build 25 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: Update ZenStack 2 | 3 | env: 4 | DO_NOT_TRACK: '1' 5 | 6 | on: 7 | workflow_dispatch: 8 | repository_dispatch: 9 | types: [zenstack-release] 10 | 11 | jobs: 12 | update: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Update to latest ZenStack 18 | run: | 19 | git config --global user.name ymc9 20 | git config --global user.email yiming@whimslab.io 21 | npm ci 22 | npm run up 23 | 24 | - name: Build 25 | run: | 26 | npm run build 27 | 28 | - name: Commit and push 29 | run: | 30 | git add . 31 | git commit -m "chore: update to latest ZenStack" || true 32 | git push || true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | *.db 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
11 | 12 | # ZenStack Todo Sample With SvelteKit 13 | 14 | This project is a collaborative Todo app built with [SvelteKit](https://kit.svelte.dev/) and [ZenStack](https://zenstack.dev). 15 | 16 | In this fictitious app, users can be invited to workspaces where they can collaborate on todos. Public todo lists are visible to all members in the workspace. 17 | 18 | ## Live deployment 19 | https://sample-todo-sveltekit.vercel.app/ 20 | 21 | ## Features 22 | 23 | - User signup/signin 24 | - Creating workspaces and inviting members 25 | - Data segregation and permission control 26 | 27 | ## Running the sample 28 | 29 | 1. Setup a new database 30 | 31 | It use PostgreSQL by default, if you want to use MySQL, simply change the db datasource provider to `mysql` in `schema.zmodel` file. 32 | 33 | You can launch a PostgreSQL instance locally, or create one from a hoster like [Supabase](https://supabase.com). Create a new database for this app, and set the connection string in .env file. 34 | 35 | 1. Install dependencies 36 | 37 | ```bash 38 | npm install 39 | ``` 40 | 41 | 1. Generate server and client-side code from model 42 | 43 | ```bash 44 | npm run generate 45 | ``` 46 | 47 | 1. Synchronize database schema 48 | 49 | ```bash 50 | npm run db:push 51 | ``` 52 | 53 | 1. Start dev server 54 | 55 | ```bash 56 | npm run dev 57 | ``` 58 | 59 | 60 | 61 | ## Feedback and Issues 62 | If you encounter any issue or have any feedback, please create a new issue in our main repository so we could respond to it promptly: 63 | 64 | [https://github.com/zenstackhq/zenstack](https://github.com/zenstackhq/zenstack) 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-sveltekit", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "generate": "zenstack generate", 7 | "dev": "vite dev", 8 | "build": "npm run generate && vite build", 9 | "vercel-build": "npm run build && npm run db:deploy", 10 | "preview": "vite preview", 11 | "db:push": "prisma db push", 12 | "db:migrate": "prisma migrate dev", 13 | "db:deploy": "prisma migrate deploy", 14 | "db:reset": "prisma migrate reset", 15 | "db:browse": "prisma studio", 16 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 17 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 18 | "package-clean": "npm rm zenstack @zenstackhq/runtime @zenstackhq/server @zenstackhq/tanstack-query", 19 | "up": "npm run package-clean && npm install -D zenstack@latest && npm install @zenstackhq/runtime@latest @zenstackhq/server@latest @zenstackhq/tanstack-query@latest", 20 | "up-preview": "npm run package-clean && npm install --registry https://preview.registry.zenstack.dev -D zenstack@latest && npm install --registry https://preview.registry.zenstack.dev @zenstackhq/runtime@latest @zenstackhq/server@latest @zenstackhq/tanstack-query@latest" 21 | }, 22 | "devDependencies": { 23 | "@sveltejs/adapter-auto": "^2.1.0", 24 | "@sveltejs/kit": "^1.22.4", 25 | "@sveltejs/package": "^2.2.0", 26 | "@tailwindcss/line-clamp": "^0.4.4", 27 | "@types/bcryptjs": "^2.4.2", 28 | "@types/jsonwebtoken": "^9.0.1", 29 | "@typescript-eslint/eslint-plugin": "^5.45.0", 30 | "@typescript-eslint/parser": "^5.45.0", 31 | "autoprefixer": "^10.4.14", 32 | "eslint": "^8.28.0", 33 | "eslint-config-prettier": "^8.5.0", 34 | "postcss": "^8.4.23", 35 | "prettier": "^2.8.0", 36 | "prettier-plugin-svelte": "^2.8.1", 37 | "prisma": "^6.5.0", 38 | "svelte": "^4.1.2", 39 | "svelte-check": "^3.4.6", 40 | "tailwindcss": "^3.3.2", 41 | "ts-node": "10.9.1", 42 | "tslib": "^2.4.1", 43 | "typescript": "^5.8.2", 44 | "vite": "^4.4.4", 45 | "zenstack": "^2.15.0" 46 | }, 47 | "type": "module", 48 | "dependencies": { 49 | "@prisma/client": "^6.5.0", 50 | "@steeze-ui/heroicons": "^2.2.3", 51 | "@steeze-ui/svelte-icon": "^1.5.0", 52 | "@tanstack/svelte-query": "^4.32.6", 53 | "@zenstackhq/runtime": "^2.15.0", 54 | "@zenstackhq/server": "^2.15.0", 55 | "@zenstackhq/tanstack-query": "^2.15.0", 56 | "bcryptjs": "^2.4.3", 57 | "daisyui": "^2.51.5", 58 | "jsonwebtoken": "^9.0.0", 59 | "moment": "^2.29.4", 60 | "nanoid": "^4.0.2", 61 | "superjson": "^1.12.3", 62 | "zod": "3.21.1", 63 | "zod-validation-error": "^1.3.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/20230905040838_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "SpaceUserRole" AS ENUM ('USER', 'ADMIN'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Space" ( 6 | "id" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL, 9 | "name" TEXT NOT NULL, 10 | "slug" TEXT NOT NULL, 11 | 12 | CONSTRAINT "Space_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateTable 16 | CREATE TABLE "SpaceUser" ( 17 | "id" TEXT NOT NULL, 18 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | "updatedAt" TIMESTAMP(3) NOT NULL, 20 | "spaceId" TEXT NOT NULL, 21 | "userId" TEXT NOT NULL, 22 | "role" "SpaceUserRole" NOT NULL, 23 | 24 | CONSTRAINT "SpaceUser_pkey" PRIMARY KEY ("id") 25 | ); 26 | 27 | -- CreateTable 28 | CREATE TABLE "User" ( 29 | "id" TEXT NOT NULL, 30 | "email" TEXT NOT NULL, 31 | "password" TEXT NOT NULL, 32 | "name" TEXT, 33 | 34 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 35 | ); 36 | 37 | -- CreateTable 38 | CREATE TABLE "List" ( 39 | "id" TEXT NOT NULL, 40 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 41 | "updatedAt" TIMESTAMP(3) NOT NULL, 42 | "spaceId" TEXT NOT NULL, 43 | "ownerId" TEXT NOT NULL, 44 | "title" TEXT NOT NULL, 45 | "private" BOOLEAN NOT NULL DEFAULT false, 46 | 47 | CONSTRAINT "List_pkey" PRIMARY KEY ("id") 48 | ); 49 | 50 | -- CreateTable 51 | CREATE TABLE "Todo" ( 52 | "id" TEXT NOT NULL, 53 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 54 | "updatedAt" TIMESTAMP(3) NOT NULL, 55 | "ownerId" TEXT NOT NULL, 56 | "listId" TEXT NOT NULL, 57 | "title" TEXT NOT NULL, 58 | "completedAt" TIMESTAMP(3), 59 | 60 | CONSTRAINT "Todo_pkey" PRIMARY KEY ("id") 61 | ); 62 | 63 | -- CreateIndex 64 | CREATE UNIQUE INDEX "Space_slug_key" ON "Space"("slug"); 65 | 66 | -- CreateIndex 67 | CREATE UNIQUE INDEX "SpaceUser_userId_spaceId_key" ON "SpaceUser"("userId", "spaceId"); 68 | 69 | -- CreateIndex 70 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 71 | 72 | -- AddForeignKey 73 | ALTER TABLE "SpaceUser" ADD CONSTRAINT "SpaceUser_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space"("id") ON DELETE CASCADE ON UPDATE CASCADE; 74 | 75 | -- AddForeignKey 76 | ALTER TABLE "SpaceUser" ADD CONSTRAINT "SpaceUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 77 | 78 | -- AddForeignKey 79 | ALTER TABLE "List" ADD CONSTRAINT "List_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space"("id") ON DELETE CASCADE ON UPDATE CASCADE; 80 | 81 | -- AddForeignKey 82 | ALTER TABLE "List" ADD CONSTRAINT "List_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 83 | 84 | -- AddForeignKey 85 | ALTER TABLE "Todo" ADD CONSTRAINT "Todo_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 86 | 87 | -- AddForeignKey 88 | ALTER TABLE "Todo" ADD CONSTRAINT "Todo_listId_fkey" FOREIGN KEY ("listId") REFERENCES "List"("id") ON DELETE CASCADE ON UPDATE CASCADE; 89 | -------------------------------------------------------------------------------- /prisma/migrations/20241222160427_add_space_owner/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `ownerId` to the `Space` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Space" ADD COLUMN "ownerId" TEXT NOT NULL; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Space" ADD CONSTRAINT "Space_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////// 2 | // DO NOT MODIFY THIS FILE // 3 | // This file is automatically generated by ZenStack CLI and should not be manually updated. // 4 | ////////////////////////////////////////////////////////////////////////////////////////////// 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | directUrl = env("DATABASE_DIRECT_URL") 10 | } 11 | 12 | generator client { 13 | provider = "prisma-client-js" 14 | } 15 | 16 | enum SpaceUserRole { 17 | USER 18 | ADMIN 19 | } 20 | 21 | model Space { 22 | id String @id() @default(uuid()) 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt() 25 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 26 | ownerId String 27 | name String 28 | slug String @unique() 29 | members SpaceUser[] 30 | lists List[] 31 | } 32 | 33 | model SpaceUser { 34 | id String @id() @default(uuid()) 35 | createdAt DateTime @default(now()) 36 | updatedAt DateTime @updatedAt() 37 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 38 | spaceId String 39 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 40 | userId String 41 | role SpaceUserRole 42 | 43 | @@unique([userId, spaceId]) 44 | } 45 | 46 | model User { 47 | id String @id() @default(cuid()) 48 | email String @unique() 49 | password String 50 | name String? 51 | ownedSpaces Space[] 52 | memberships SpaceUser[] 53 | todos Todo[] 54 | lists List[] 55 | } 56 | 57 | model List { 58 | id String @id() @default(uuid()) 59 | createdAt DateTime @default(now()) 60 | updatedAt DateTime @updatedAt() 61 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 62 | spaceId String 63 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 64 | ownerId String 65 | title String 66 | private Boolean @default(false) 67 | todos Todo[] 68 | } 69 | 70 | model Todo { 71 | id String @id() @default(uuid()) 72 | createdAt DateTime @default(now()) 73 | updatedAt DateTime @updatedAt() 74 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 75 | ownerId String 76 | list List @relation(fields: [listId], references: [id], onDelete: Cascade) 77 | listId String 78 | title String 79 | completedAt DateTime? 80 | } 81 | -------------------------------------------------------------------------------- /schema.zmodel: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = 'postgresql' 7 | url = env('DATABASE_URL') 8 | directUrl = env('DATABASE_DIRECT_URL') 9 | } 10 | 11 | /* 12 | * Enum for user's role in a space 13 | */ 14 | enum SpaceUserRole { 15 | USER 16 | ADMIN 17 | } 18 | 19 | plugin hooks { 20 | provider = '@zenstackhq/tanstack-query' 21 | output = 'src/lib/hooks' 22 | target = 'svelte' 23 | } 24 | 25 | /* 26 | * Model for a space in which users can collaborate on Lists and Todos 27 | */ 28 | model Space { 29 | id String @id @default(uuid()) 30 | createdAt DateTime @default(now()) 31 | updatedAt DateTime @updatedAt 32 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 33 | ownerId String @default(auth().id) 34 | name String @length(4, 50) 35 | slug String @unique @regex('^[0-9a-zA-Z\-_]{4,16}$') 36 | members SpaceUser[] 37 | lists List[] 38 | 39 | // require login 40 | @@deny('all', auth() == null) 41 | 42 | // everyone can create a space 43 | @@allow('create', true) 44 | 45 | // any user in the space can read the space 46 | @@allow('read', members?[user == auth()]) 47 | 48 | // space admin can update and delete 49 | @@allow('update,delete', members?[user == auth() && role == ADMIN]) 50 | } 51 | 52 | /* 53 | * Model representing membership of a user in a space 54 | */ 55 | model SpaceUser { 56 | id String @id @default(uuid()) 57 | createdAt DateTime @default(now()) 58 | updatedAt DateTime @updatedAt 59 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 60 | spaceId String 61 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 62 | userId String 63 | role SpaceUserRole 64 | @@unique([userId, spaceId]) 65 | 66 | // require login 67 | @@deny('all', auth() == null) 68 | 69 | // space owner can add any one 70 | @@allow('create', space.owner == auth()) 71 | 72 | // space admin can add anyone but not himself 73 | @@allow('create', auth() != user && space.members?[user == auth() && role == ADMIN]) 74 | 75 | // space admin can update/delete 76 | @@allow('update,delete', space.members?[user == auth() && role == ADMIN]) 77 | 78 | // user can read entries for spaces which he's a member of 79 | @@allow('read', space.members?[user == auth()]) 80 | } 81 | 82 | /* 83 | * User model 84 | */ 85 | model User { 86 | id String @id @default(cuid()) 87 | email String @unique @email 88 | password String @password @omit @length(6, 32) 89 | name String? 90 | ownedSpaces Space[] 91 | memberships SpaceUser[] 92 | todos Todo[] 93 | lists List[] 94 | @@allow('create,read', true) 95 | @@allow('all', auth() == this) 96 | } 97 | 98 | /* 99 | * Model for a Todo list 100 | */ 101 | model List { 102 | id String @id @default(uuid()) 103 | createdAt DateTime @default(now()) 104 | updatedAt DateTime @updatedAt 105 | space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) 106 | spaceId String 107 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 108 | ownerId String @default(auth().id) 109 | title String @length(1, 100) 110 | private Boolean @default(false) 111 | todos Todo[] 112 | 113 | // require login 114 | @@deny('all', auth() == null) 115 | 116 | // can be read by owner or space members (only if not private) 117 | @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) 118 | 119 | // when create, owner must be set to current user, and user must be in the space 120 | @@allow('create', owner == auth() && space.members?[user == auth()]) 121 | 122 | // when create, owner must be set to current user, and user must be in the space 123 | // update is not allowed to change owner 124 | @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner) 125 | 126 | // can be deleted by owner 127 | @@allow('delete', owner == auth()) 128 | } 129 | 130 | /* 131 | * Model for a single Todo 132 | */ 133 | model Todo { 134 | id String @id @default(uuid()) 135 | createdAt DateTime @default(now()) 136 | updatedAt DateTime @updatedAt 137 | owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) 138 | ownerId String @default(auth().id) 139 | list List @relation(fields: [listId], references: [id], onDelete: Cascade) 140 | listId String 141 | title String @length(1, 100) 142 | completedAt DateTime? 143 | 144 | // require login 145 | @@deny('all', auth() == null) 146 | 147 | // owner has full access, also space members have full access (if the parent List is not private) 148 | @@allow('all', list.owner == auth()) 149 | @@allow('all', list.space.members?[user == auth()] && !list.private) 150 | 151 | // update cannot change owner 152 | @@deny('update', future().owner != owner) 153 | } 154 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | 4 | import type { PrismaClient, User } from '@prisma/client'; 5 | 6 | declare global { 7 | namespace App { 8 | interface Locals { 9 | user?: User; 10 | db: PrismaClient; 11 | } 12 | } 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |100 | {member.user.name || member.user.email} 101 |
102 |{member.role}
103 |12 | {value.completedAt 13 | ? `Completed ${moment(value.completedAt).fromNow()}` 14 | : value.createdAt === value.updatedAt 15 | ? `Created ${moment(value.createdAt).fromNow()}` 16 | : `Updated ${moment(value.updatedAt).fromNow()}`} 17 |
18 | -------------------------------------------------------------------------------- /src/lib/components/Todo.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |