diff --git a/client/.gitignore b/client/.gitignore
new file mode 100644
index 0000000..d20567f
--- /dev/null
+++ b/client/.gitignore
@@ -0,0 +1,7 @@
+# Node
+node_modules
+package-lock.json
+yarn.lock
+
+# Sapper
+__sapper__
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..a1a266a
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,26 @@
+{
+ "scripts": {
+ "dev": "sapper dev",
+ "build": "sapper build --legacy",
+ "export": "sapper export --legacy",
+ "start": "node __sapper__/build"
+ },
+ "dependencies": {
+ "axios": "^0.19.2",
+ "bootstrap": "^4.5.0",
+ "compression": "^1.7.1",
+ "polka": "next",
+ "sirv": "^0.4.0"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^12.0.0",
+ "@rollup/plugin-node-resolve": "^8.0.0",
+ "@rollup/plugin-replace": "^2.2.0",
+ "npm-run-all": "^4.1.5",
+ "rollup": "^2.3.4",
+ "rollup-plugin-svelte": "^5.0.1",
+ "rollup-plugin-terser": "^5.3.0",
+ "sapper": "^0.27.0",
+ "svelte": "^3.0.0"
+ }
+}
diff --git a/client/rollup.config.js b/client/rollup.config.js
new file mode 100644
index 0000000..ef6206b
--- /dev/null
+++ b/client/rollup.config.js
@@ -0,0 +1,89 @@
+import resolve from '@rollup/plugin-node-resolve'
+import replace from '@rollup/plugin-replace'
+import commonjs from '@rollup/plugin-commonjs'
+import svelte from 'rollup-plugin-svelte'
+import { terser } from 'rollup-plugin-terser'
+import config from 'sapper/config/rollup.js'
+import pkg from './package.json'
+
+const mode = process.env.NODE_ENV
+const dev = mode === 'development'
+
+const onwarn = (warning, onwarn) =>
+ (warning.code === 'CIRCULAR_DEPENDENCY' &&
+ /[/\\]@sapper[/\\]/.test(warning.message)) ||
+ onwarn(warning)
+
+export default {
+ client: {
+ input: config.client.input(),
+ output: config.client.output(),
+ plugins: [
+ replace({
+ 'process.browser': true,
+ 'process.env.NODE_ENV': JSON.stringify(mode),
+ }),
+ svelte({
+ dev,
+ hydratable: true,
+ emitCss: true,
+ }),
+ resolve({
+ browser: true,
+ dedupe: ['svelte'],
+ }),
+ commonjs(),
+
+ !dev &&
+ terser({
+ module: true,
+ }),
+ ],
+
+ preserveEntrySignatures: false,
+ onwarn,
+ },
+
+ server: {
+ input: config.server.input(),
+ output: config.server.output(),
+ plugins: [
+ replace({
+ 'process.browser': false,
+ 'process.env.NODE_ENV': JSON.stringify(mode),
+ }),
+ svelte({
+ generate: 'ssr',
+ dev,
+ }),
+ resolve({
+ dedupe: ['svelte'],
+ }),
+ commonjs(),
+ ],
+ external: Object.keys(pkg.dependencies).concat(
+ require('module').builtinModules ||
+ Object.keys(process.binding('natives'))
+ ),
+
+ preserveEntrySignatures: 'strict',
+ onwarn,
+ },
+
+ serviceworker: {
+ input: config.serviceworker.input(),
+ output: config.serviceworker.output(),
+ plugins: [
+ resolve(),
+ replace({
+ 'process.browser': true,
+ 'process.env.NODE_ENV': JSON.stringify(mode),
+ }),
+ commonjs(),
+ !dev && terser(),
+ ],
+
+ preserveEntrySignatures: false,
+ onwarn,
+ },
+}
diff --git a/client/src/api/index.js b/client/src/api/index.js
new file mode 100644
index 0000000..5338045
--- /dev/null
+++ b/client/src/api/index.js
@@ -0,0 +1,39 @@
+import { writable } from 'svelte/store'
+import axios from 'axios'
+
+axios.defaults.baseURL = '//localhost:8000'
+
+export const todos = writable([])
+
+export async function refresh() {
+ const { data } = await axios({
+ url: '/todo',
+ })
+ todos.set(data)
+}
+
+export async function add(todo) {
+ await axios({
+ url: '/todo',
+ method: 'post',
+ data: todo,
+ })
+ await refresh()
+}
+
+export async function remove(id) {
+ await axios({
+ url: '/todo/' + id,
+ method: 'delete',
+ })
+ await refresh()
+}
+
+export async function update({ _id, ...todo }) {
+ await axios({
+ url: '/todo/' + _id,
+ method: 'post',
+ data: todo,
+ })
+ await refresh()
+}
diff --git a/client/src/client.js b/client/src/client.js
new file mode 100644
index 0000000..cec9172
--- /dev/null
+++ b/client/src/client.js
@@ -0,0 +1,5 @@
+import * as sapper from '@sapper/app';
+
+sapper.start({
+ target: document.querySelector('#sapper')
+});
\ No newline at end of file
diff --git a/client/src/components/Add.svelte b/client/src/components/Add.svelte
new file mode 100644
index 0000000..2948d2e
--- /dev/null
+++ b/client/src/components/Add.svelte
@@ -0,0 +1,31 @@
+
+
+
+
+
Add
+
+
diff --git a/client/src/components/Button.svelte b/client/src/components/Button.svelte
new file mode 100644
index 0000000..978e915
--- /dev/null
+++ b/client/src/components/Button.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/client/src/components/Field.svelte b/client/src/components/Field.svelte
new file mode 100644
index 0000000..acfb390
--- /dev/null
+++ b/client/src/components/Field.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/client/src/components/List.svelte b/client/src/components/List.svelte
new file mode 100644
index 0000000..4dded7f
--- /dev/null
+++ b/client/src/components/List.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+List
+
+
+ {#each $todos as todo (todo._id)}
+
+ {/each}
+
diff --git a/client/src/components/Todo.svelte b/client/src/components/Todo.svelte
new file mode 100644
index 0000000..c37eff3
--- /dev/null
+++ b/client/src/components/Todo.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
diff --git a/client/src/routes/_layout.svelte b/client/src/routes/_layout.svelte
new file mode 100644
index 0000000..7e16760
--- /dev/null
+++ b/client/src/routes/_layout.svelte
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/client/src/routes/index.svelte b/client/src/routes/index.svelte
new file mode 100644
index 0000000..9709de6
--- /dev/null
+++ b/client/src/routes/index.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Todos
+
+
+Todos
+
+
+
+
+
+
diff --git a/client/src/routes/todo/[id].svelte b/client/src/routes/todo/[id].svelte
new file mode 100644
index 0000000..b8429d4
--- /dev/null
+++ b/client/src/routes/todo/[id].svelte
@@ -0,0 +1,18 @@
+
+
+
+
+{#if todo}
+
+{/if}
diff --git a/client/src/server.js b/client/src/server.js
new file mode 100644
index 0000000..c77f593
--- /dev/null
+++ b/client/src/server.js
@@ -0,0 +1,17 @@
+import sirv from 'sirv';
+import polka from 'polka';
+import compression from 'compression';
+import * as sapper from '@sapper/server';
+
+const { PORT, NODE_ENV } = process.env;
+const dev = NODE_ENV === 'development';
+
+polka() // You can also use Express
+ .use(
+ compression({ threshold: 0 }),
+ sirv('static', { dev }),
+ sapper.middleware()
+ )
+ .listen(PORT, err => {
+ if (err) console.log('error', err);
+ });
diff --git a/client/src/service-worker.js b/client/src/service-worker.js
new file mode 100644
index 0000000..2289a55
--- /dev/null
+++ b/client/src/service-worker.js
@@ -0,0 +1,82 @@
+import { timestamp, files, shell, routes } from '@sapper/service-worker';
+
+const ASSETS = `cache${timestamp}`;
+
+// `shell` is an array of all the files generated by the bundler,
+// `files` is an array of everything in the `static` directory
+const to_cache = shell.concat(files);
+const cached = new Set(to_cache);
+
+self.addEventListener('install', event => {
+ event.waitUntil(
+ caches
+ .open(ASSETS)
+ .then(cache => cache.addAll(to_cache))
+ .then(() => {
+ self.skipWaiting();
+ })
+ );
+});
+
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ caches.keys().then(async keys => {
+ // delete old caches
+ for (const key of keys) {
+ if (key !== ASSETS) await caches.delete(key);
+ }
+
+ self.clients.claim();
+ })
+ );
+});
+
+self.addEventListener('fetch', event => {
+ if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
+
+ const url = new URL(event.request.url);
+
+ // don't try to handle e.g. data: URIs
+ if (!url.protocol.startsWith('http')) return;
+
+ // ignore dev server requests
+ if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
+
+ // always serve static files and bundler-generated assets from cache
+ if (url.host === self.location.host && cached.has(url.pathname)) {
+ event.respondWith(caches.match(event.request));
+ return;
+ }
+
+ // for pages, you might want to serve a shell `service-worker-index.html` file,
+ // which Sapper has generated for you. It's not right for every
+ // app, but if it's right for yours then uncomment this section
+ /*
+ if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
+ event.respondWith(caches.match('/service-worker-index.html'));
+ return;
+ }
+ */
+
+ if (event.request.cache === 'only-if-cached') return;
+
+ // for everything else, try the network first, falling back to
+ // cache if the user is offline. (If the pages never change, you
+ // might prefer a cache-first approach to a network-first one.)
+ event.respondWith(
+ caches
+ .open(`offline${timestamp}`)
+ .then(async cache => {
+ try {
+ const response = await fetch(event.request);
+ cache.put(event.request, response.clone());
+ return response;
+ } catch(err) {
+ const response = await cache.match(event.request);
+ if (response) return response;
+
+ throw err;
+ }
+ })
+ );
+});
diff --git a/client/src/template.html b/client/src/template.html
new file mode 100644
index 0000000..c4f84ae
--- /dev/null
+++ b/client/src/template.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ %sapper.base%
+
+
+ %sapper.styles%
+
+
+ %sapper.head%
+
+
+
+ %sapper.html%
+
+
+ %sapper.scripts%
+
+