diff --git a/examples/todomvc/.gitignore b/examples/todomvc/.gitignore new file mode 100644 index 0000000..6635cf5 --- /dev/null +++ b/examples/todomvc/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/examples/todomvc/.npmrc b/examples/todomvc/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/examples/todomvc/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/examples/todomvc/.prettierignore b/examples/todomvc/.prettierignore new file mode 100644 index 0000000..3897265 --- /dev/null +++ b/examples/todomvc/.prettierignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/examples/todomvc/.prettierrc b/examples/todomvc/.prettierrc new file mode 100644 index 0000000..a77fdde --- /dev/null +++ b/examples/todomvc/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/examples/todomvc/README.md b/examples/todomvc/README.md new file mode 100644 index 0000000..6e06126 --- /dev/null +++ b/examples/todomvc/README.md @@ -0,0 +1,26 @@ +# Todo MVC + +An example implementation of the Todo MVC app. It uses SvelteKit's [format actions](https://kit.svelte.dev/docs/form-actions). It uses progressive enhancement, which means the app is still functional without JavaScript - but when it _is_ available, it provides a nicer experience by having optimistic UI updates, for example showing the new TODO item before it actually exists in the database. + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json new file mode 100644 index 0000000..4ba3639 --- /dev/null +++ b/examples/todomvc/package.json @@ -0,0 +1,28 @@ +{ + "name": "todomvc", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check .", + "format": "prettier --plugin-search-dir . --write ." + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.12.0", + "@sveltejs/package": "^2.0.0", + "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.8.1", + "publint": "^0.1.9", + "svelte": "^3.57.0", + "svelte-check": "^3.0.1", + "tslib": "^2.4.1", + "typescript": "^4.9.3", + "vite": "^4.0.0" + }, + "type": "module" +} diff --git a/examples/todomvc/src/app.d.ts b/examples/todomvc/src/app.d.ts new file mode 100644 index 0000000..26a9569 --- /dev/null +++ b/examples/todomvc/src/app.d.ts @@ -0,0 +1,9 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +// and what to do when importing types +declare namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} +} diff --git a/examples/todomvc/src/app.html b/examples/todomvc/src/app.html new file mode 100644 index 0000000..effe0d0 --- /dev/null +++ b/examples/todomvc/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/todomvc/src/routes/+page.server.ts b/examples/todomvc/src/routes/+page.server.ts new file mode 100644 index 0000000..d405de8 --- /dev/null +++ b/examples/todomvc/src/routes/+page.server.ts @@ -0,0 +1,56 @@ +import type { Actions, PageServerLoad } from './$types'; +import { + addTodo, + clearCompleted, + deleteTodo, + editTodo, + getTodos, + toggleAll, + toggleTodo +} from './db.server'; + +const wait = async () => { + return new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); +}; + +export const load = (() => { + return { todos: getTodos() }; +}) satisfies PageServerLoad; + +export const actions = { + addTodo: async ({ request }) => { + await wait(); + const form = await request.formData(); + const title = form.get('title') as string; + addTodo(title); + }, + deleteTodo: async ({ request }) => { + await wait(); + const form = await request.formData(); + const id = form.get('id') as string; + deleteTodo(id); + }, + editTodo: async ({ request }) => { + await wait(); + const form = await request.formData(); + const id = form.get('id') as string; + const title = form.get('title') as string; + editTodo(id, title); + }, + toggleTodo: async ({ request }) => { + await wait(); + const form = await request.formData(); + const id = form.get('id') as string; + toggleTodo(id); + }, + toggleAll: async ({ request }) => { + await wait(); + const form = await request.formData(); + const completed = form.get('completed') === 'true'; + toggleAll(completed); + }, + clearCompleted: async () => { + await wait(); + clearCompleted(); + } +} satisfies Actions; diff --git a/examples/todomvc/src/routes/+page.svelte b/examples/todomvc/src/routes/+page.svelte new file mode 100644 index 0000000..6f29c35 --- /dev/null +++ b/examples/todomvc/src/routes/+page.svelte @@ -0,0 +1,182 @@ + + + + Todos + + + +
+
+

todos

+
+ +
+
+ + {#if todos.length > 0} +
+
+ +
+ + {/if} + +
+ {/if} +
diff --git a/examples/todomvc/src/routes/db.server.ts b/examples/todomvc/src/routes/db.server.ts new file mode 100644 index 0000000..0cb2e56 --- /dev/null +++ b/examples/todomvc/src/routes/db.server.ts @@ -0,0 +1,44 @@ +// Todos are managed managed in memory, so we don't need a database. We can't deploy this +// to the edge/serverless environments for that reason, but it's fine for local development. + +export interface Todo { + id: string; + title: string; + completed: boolean; +} + +let todos: Todo[] = []; + +export function getTodos() { + return todos; +} + +export function addTodo(title: string) { + todos.push({ id: 'id-' + Math.random(), title, completed: false }); +} + +export function deleteTodo(id: string) { + todos = todos.filter((todo) => todo.id !== id); +} + +export function editTodo(id: string, title: string) { + const todo = todos.find((todo) => todo.id === id); + if (todo) { + todo.title = title; + } +} + +export function toggleTodo(id: string) { + const todo = todos.find((todo) => todo.id === id); + if (todo) { + todo.completed = !todo.completed; + } +} + +export function toggleAll(completed: boolean) { + todos.forEach((todo) => (todo.completed = completed)); +} + +export function clearCompleted() { + todos = todos.filter((todo) => !todo.completed); +} diff --git a/examples/todomvc/src/styles.css b/examples/todomvc/src/styles.css new file mode 100644 index 0000000..ef31997 --- /dev/null +++ b/examples/todomvc/src/styles.css @@ -0,0 +1,334 @@ +/* The original TODO-MVC CSS, minimally adjusted to enable progressive enhancement */ + +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.edit { + padding: 12px 16px; +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -5px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all.checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; + display: flex; + align-items: center; + height: 58px; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.pending { + opacity: 0.5; +} + +.todo-list li .text { + height: 58px; + flex: 100% 1 1; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + height: 40px; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li.completed .toggle { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed .edit { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/examples/todomvc/static/favicon.png b/examples/todomvc/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/examples/todomvc/static/favicon.png differ diff --git a/examples/todomvc/svelte.config.js b/examples/todomvc/svelte.config.js new file mode 100644 index 0000000..87f198f --- /dev/null +++ b/examples/todomvc/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/kit/vite'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + adapter: adapter() + } +}; + +export default config; diff --git a/examples/todomvc/tsconfig.json b/examples/todomvc/tsconfig.json new file mode 100644 index 0000000..6ae0c8c --- /dev/null +++ b/examples/todomvc/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/examples/todomvc/vite.config.js b/examples/todomvc/vite.config.js new file mode 100644 index 0000000..8747050 --- /dev/null +++ b/examples/todomvc/vite.config.js @@ -0,0 +1,8 @@ +import { sveltekit } from '@sveltejs/kit/vite'; + +/** @type {import('vite').UserConfig} */ +const config = { + plugins: [sveltekit()] +}; + +export default config;