style: config prettier

This commit is contained in:
Reinaldy Rafli 2021-07-09 22:44:24 +07:00
parent 168bb36883
commit 9bb26c1279
17 changed files with 659 additions and 774 deletions

View File

@ -1,6 +1,10 @@
{ {
"useTabs": true, "useTabs": false,
"endOfLine": "lf",
"arrowParens": "always",
"semi": true,
"tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "es5",
"printWidth": 100 "printWidth": 120
} }

View File

@ -1,47 +1,47 @@
{ {
"name": "jokesbapak2-client", "name": "jokesbapak2-client",
"version": "0.0.1", "version": "0.0.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"contributors": [ "contributors": [
{ {
"name": "Reinaldy Rafli", "name": "Reinaldy Rafli",
"email": "aldy505@tutanota.com", "email": "aldy505@tutanota.com",
"url": "https://github.com/aldy505" "url": "https://github.com/aldy505"
} }
], ],
"scripts": { "scripts": {
"dev": "svelte-kit dev", "dev": "svelte-kit dev",
"build": "svelte-kit build", "build": "svelte-kit build",
"preview": "svelte-kit preview", "preview": "svelte-kit preview",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint --fix --ext .svelte,.js,.ts --ignore-path .gitignore .", "lint": "eslint --fix --ext .svelte,.js,.ts --ignore-path .gitignore .",
"format": "prettier --write --ignore-path .gitignore --plugin-search-dir=. \"./**/*.(ts|json|js|svelte)\"" "format": "prettier --write --ignore-path .gitignore --plugin-search-dir=. \"./**/*.(ts|json|js|svelte)\""
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.13", "@sveltejs/adapter-static": "^1.0.0-next.13",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"@types/cookie": "^0.4.0", "@types/cookie": "^0.4.0",
"@typescript-eslint/eslint-plugin": "^4.19.0", "@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0", "@typescript-eslint/parser": "^4.19.0",
"cssnano": "^5.0.6", "cssnano": "^5.0.6",
"eslint": "^7.22.0", "eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-svelte3": "^3.2.0", "eslint-plugin-svelte3": "^3.2.0",
"prettier": "~2.2.1", "prettier": "~2.2.1",
"prettier-plugin-svelte": "^2.2.0", "prettier-plugin-svelte": "^2.2.0",
"svelte": "^3.34.0", "svelte": "^3.34.0",
"svelte-check": "^2.0.0", "svelte-check": "^2.0.0",
"svelte-preprocess": "^4.7.3", "svelte-preprocess": "^4.7.3",
"svelte-windicss-preprocess": "^4.0.12", "svelte-windicss-preprocess": "^4.0.12",
"tailwindcss": "^2.2.4", "tailwindcss": "^2.2.4",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"typescript": "^4.0.0" "typescript": "^4.0.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.2.2", "@fontsource/fira-mono": "^4.2.2",
"@lukeed/uuid": "^2.0.0", "@lukeed/uuid": "^2.0.0",
"cookie": "^0.4.1" "cookie": "^0.4.1"
} }
} }

View File

@ -1,110 +0,0 @@
/* Write your global styles here, in PostCSS syntax */
@import '@fontsource/fira-mono';
:root {
font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Mono', monospace;
--pure-white: #ffffff;
--primary-color: #b9c6d2;
--secondary-color: #d0dde9;
--tertiary-color: #edf0f8;
--accent-color: #ff3e00;
--heading-color: rgba(0, 0, 0, 0.7);
--text-color: #444444;
--background-without-opacity: rgba(255, 255, 255, 0.7);
--column-width: 42rem;
--column-margin-top: 4rem;
}
body {
min-height: 100vh;
margin: 0;
background-color: var(--primary-color);
background: linear-gradient(
180deg,
var(--primary-color) 0%,
var(--secondary-color) 10.45%,
var(--tertiary-color) 41.35%
);
}
body::before {
content: '';
width: 80vw;
height: 100vh;
position: absolute;
top: 0;
left: 10vw;
z-index: -1;
background: radial-gradient(
50% 50% at 50% 50%,
var(--pure-white) 0%,
rgba(255, 255, 255, 0) 100%
);
opacity: 0.05;
}
#svelte {
min-height: 100vh;
display: flex;
flex-direction: column;
}
h1,
h2,
p {
font-weight: 400;
color: var(--heading-color);
}
p {
line-height: 1.5;
}
a {
color: var(--accent-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1 {
font-size: 2rem;
margin-bottom: 0 0 1em 0;
text-align: center;
}
h2 {
font-size: 1rem;
}
pre {
font-size: 16px;
font-family: var(--font-mono);
background-color: rgba(255, 255, 255, 0.45);
border-radius: 3px;
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
padding: 0.5em;
overflow-x: auto;
color: var(--text-color);
}
input,
button {
font-size: inherit;
font-family: inherit;
}
button:focus:not(:focus-visible) {
outline: none;
}
@media (min-width: 720px) {
h1 {
font-size: 2.4rem;
}
}

View File

@ -3,21 +3,21 @@ import { v4 as uuid } from '@lukeed/uuid';
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ request, resolve }) => { export const handle: Handle = async ({ request, resolve }) => {
const cookies = cookie.parse(request.headers.cookie || ''); const cookies = cookie.parse(request.headers.cookie || '');
request.locals.userid = cookies.userid || uuid(); request.locals.userid = cookies.userid || uuid();
// TODO https://github.com/sveltejs/kit/issues/1046 // TODO https://github.com/sveltejs/kit/issues/1046
if (request.query.has('_method')) { if (request.query.has('_method')) {
request.method = request.query.get('_method').toUpperCase(); request.method = request.query.get('_method').toUpperCase();
} }
const response = await resolve(request); const response = await resolve(request);
if (!cookies.userid) { if (!cookies.userid) {
// if this is the first time the user has visited this app, // if this is the first time the user has visited this app,
// set a cookie so that we recognise them when they return // set a cookie so that we recognise them when they return
response.headers['set-cookie'] = `userid=${request.locals.userid}; Path=/; HttpOnly`; response.headers['set-cookie'] = `userid=${request.locals.userid}; Path=/; HttpOnly`;
} }
return response; return response;
}; };

View File

@ -1,98 +1,98 @@
<script lang="ts"> <script lang="ts">
import { spring } from 'svelte/motion'; import { spring } from 'svelte/motion';
let count = 0; let count = 0;
const displayed_count = spring(); const displayed_count = spring();
$: displayed_count.set(count); $: displayed_count.set(count);
$: offset = modulo($displayed_count, 1); $: offset = modulo($displayed_count, 1);
function modulo(n: number, m: number) { function modulo(n: number, m: number) {
// handle negative numbers // handle negative numbers
return ((n % m) + m) % m; return ((n % m) + m) % m;
} }
</script> </script>
<div class="counter"> <div class="counter">
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one"> <button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1"> <svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" /> <path d="M0,0.5 L1,0.5" />
</svg> </svg>
</button> </button>
<div class="counter-viewport"> <div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)"> <div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong style="top: -100%" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong> <strong style="top: -100%" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong> <strong>{Math.floor($displayed_count)}</strong>
</div> </div>
</div> </div>
<button on:click={() => (count += 1)} aria-label="Increase the counter by one"> <button on:click={() => (count += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1"> <svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" /> <path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg> </svg>
</button> </button>
</div> </div>
<style> <style>
.counter { .counter {
display: flex; display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0; margin: 1rem 0;
} }
.counter button { .counter button {
width: 2em; width: 2em;
padding: 0; padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 0; border: 0;
background-color: transparent; background-color: transparent;
color: var(--text-color); color: var(--text-color);
font-size: 2rem; font-size: 2rem;
} }
.counter button:hover { .counter button:hover {
background-color: var(--secondary-color); background-color: var(--secondary-color);
} }
svg { svg {
width: 25%; width: 25%;
height: 25%; height: 25%;
} }
path { path {
vector-effect: non-scaling-stroke; vector-effect: non-scaling-stroke;
stroke-width: 2px; stroke-width: 2px;
stroke: var(--text-color); stroke: var(--text-color);
} }
.counter-viewport { .counter-viewport {
width: 8em; width: 8em;
height: 4em; height: 4em;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
position: relative; position: relative;
} }
.counter-viewport strong { .counter-viewport strong {
position: absolute; position: absolute;
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
font-weight: 400; font-weight: 400;
color: var(--accent-color); color: var(--accent-color);
font-size: 4rem; font-size: 4rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.counter-digits { .counter-digits {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
</style> </style>

View File

@ -1,120 +1,120 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import logo from './svelte-logo.svg'; import logo from './svelte-logo.svg';
</script> </script>
<header> <header>
<div class="corner"> <div class="corner">
<a href="https://kit.svelte.dev"> <a href="https://kit.svelte.dev">
<img src={logo} alt="SvelteKit" /> <img src={logo} alt="SvelteKit" />
</a> </a>
</div> </div>
<nav> <nav>
<svg viewBox="0 0 2 3" aria-hidden="true"> <svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" /> <path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg> </svg>
<ul> <ul>
<li class:active={$page.path === '/'}><a sveltekit:prefetch href="/">Home</a></li> <li class:active={$page.path === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.path === '/about'}><a sveltekit:prefetch href="/about">About</a></li> <li class:active={$page.path === '/about'}><a sveltekit:prefetch href="/about">About</a></li>
<li class:active={$page.path === '/todos'}><a sveltekit:prefetch href="/todos">Todos</a></li> <li class:active={$page.path === '/todos'}><a sveltekit:prefetch href="/todos">Todos</a></li>
</ul> </ul>
<svg viewBox="0 0 2 3" aria-hidden="true"> <svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" /> <path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
</svg> </svg>
</nav> </nav>
<div class="corner"> <div class="corner">
<!-- TODO put something else here? github link? --> <!-- TODO put something else here? github link? -->
</div> </div>
</header> </header>
<style> <style>
header { header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.corner { .corner {
width: 3em; width: 3em;
height: 3em; height: 3em;
} }
.corner a { .corner a {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.corner img { .corner img {
width: 2em; width: 2em;
height: 2em; height: 2em;
object-fit: contain; object-fit: contain;
} }
nav { nav {
display: flex; display: flex;
justify-content: center; justify-content: center;
--background: rgba(255, 255, 255, 0.7); --background: rgba(255, 255, 255, 0.7);
} }
svg { svg {
width: 2em; width: 2em;
height: 3em; height: 3em;
display: block; display: block;
} }
path { path {
fill: var(--background); fill: var(--background);
} }
ul { ul {
position: relative; position: relative;
padding: 0; padding: 0;
margin: 0; margin: 0;
height: 3em; height: 3em;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
list-style: none; list-style: none;
background: var(--background); background: var(--background);
background-size: contain; background-size: contain;
} }
li { li {
position: relative; position: relative;
height: 100%; height: 100%;
} }
li.active::before { li.active::before {
--size: 6px; --size: 6px;
content: ''; content: '';
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;
top: 0; top: 0;
left: calc(50% - var(--size)); left: calc(50% - var(--size));
border: var(--size) solid transparent; border: var(--size) solid transparent;
border-top: var(--size) solid var(--accent-color); border-top: var(--size) solid var(--accent-color);
} }
nav a { nav a {
display: flex; display: flex;
height: 100%; height: 100%;
align-items: center; align-items: center;
padding: 0 1em; padding: 0 1em;
color: var(--heading-color); color: var(--heading-color);
font-weight: 700; font-weight: 700;
font-size: 0.8rem; font-size: 0.8rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 10%; letter-spacing: 10%;
text-decoration: none; text-decoration: none;
transition: color 0.2s linear; transition: color 0.2s linear;
} }
a:hover { a:hover {
color: var(--accent-color); color: var(--accent-color);
} }
</style> </style>

View File

@ -1,60 +1,60 @@
// this action (https://svelte.dev/tutorial/actions) allows us to // this action (https://svelte.dev/tutorial/actions) allows us to
// progressively enhance a <form> that already works without JS // progressively enhance a <form> that already works without JS
export function enhance( export function enhance(
form: HTMLFormElement, form: HTMLFormElement,
{ {
pending, pending,
error, error,
result result,
}: { }: {
pending?: (data: FormData, form: HTMLFormElement) => void; pending?: (data: FormData, form: HTMLFormElement) => void;
error?: (res: Response, error: Error, form: HTMLFormElement) => void; error?: (res: Response, error: Error, form: HTMLFormElement) => void;
result: (res: Response, form: HTMLFormElement) => void; result: (res: Response, form: HTMLFormElement) => void;
} }
) { ) {
let current_token: {}; let current_token: {};
async function handle_submit(e: Event) { async function handle_submit(e: Event) {
const token = (current_token = {}); const token = (current_token = {});
e.preventDefault(); e.preventDefault();
const body = new FormData(form); const body = new FormData(form);
if (pending) pending(body, form); if (pending) pending(body, form);
try { try {
const res = await fetch(form.action, { const res = await fetch(form.action, {
method: form.method, method: form.method,
headers: { headers: {
accept: 'application/json' accept: 'application/json',
}, },
body body,
}); });
if (token !== current_token) return; if (token !== current_token) return;
if (res.ok) { if (res.ok) {
result(res, form); result(res, form);
} else if (error) { } else if (error) {
error(res, null, form); error(res, null, form);
} else { } else {
console.error(await res.text()); console.error(await res.text());
} }
} catch (e) { } catch (e) {
if (error) { if (error) {
error(null, e, form); error(null, e, form);
} else { } else {
throw e; throw e;
} }
} }
} }
form.addEventListener('submit', handle_submit); form.addEventListener('submit', handle_submit);
return { return {
destroy() { destroy() {
form.removeEventListener('submit', handle_submit); form.removeEventListener('submit', handle_submit);
} },
}; };
} }

View File

@ -3,5 +3,5 @@
* inside `global.d.ts` and removing `export` keyword * inside `global.d.ts` and removing `export` keyword
*/ */
export interface Locals { export interface Locals {
userid: string; userid: string;
} }

View File

@ -1,46 +1,46 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/Header/index.svelte'; import Header from '$lib/Header/index.svelte';
import '../app.postcss'; import '../app.postcss';
</script> </script>
<Header> <Header>
<main> <main>
<slot /> <slot />
</main> </main>
<footer> <footer>
<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p> <p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
</footer> </footer>
<style windi:preflights:global windi:safelist:global> <style windi:preflights:global windi:safelist:global>
main { main {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem; padding: 1rem;
width: 100%; width: 100%;
max-width: 1024px; max-width: 1024px;
margin: 0 auto; margin: 0 auto;
box-sizing: border-box; box-sizing: border-box;
} }
footer { footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 40px; padding: 40px;
} }
footer a { footer a {
font-weight: bold; font-weight: bold;
} }
@media (min-width: 480px) { @media (min-width: 480px) {
footer { footer {
padding: 40px 0; padding: 40px 0;
} }
} }
</style> </style>
</Header> </Header>
<slot /> <slot />

View File

@ -1,50 +1,50 @@
<script context="module"> <script context="module">
import { browser, dev } from '$app/env'; import { browser, dev } from '$app/env';
// we don't need any JS on this page, though we'll load // we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement... // it in dev so that we get hot module replacement...
export const hydrate = dev; export const hydrate = dev;
// ...but if the client-side router is already loaded // ...but if the client-side router is already loaded
// (i.e. we came here from elsewhere in the app), use it // (i.e. we came here from elsewhere in the app), use it
export const router = browser; export const router = browser;
// since there's no dynamic data here, we can prerender // since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in prod // it so that it gets served as a static asset in prod
export const prerender = true; export const prerender = true;
</script> </script>
<svelte:head> <svelte:head>
<title>About</title> <title>About</title>
</svelte:head> </svelte:head>
<div class="content"> <div class="content">
<h1>About this app</h1> <h1>About this app</h1>
<p> <p>
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the following into your
following into your command line and following the prompts: command line and following the prompts:
</p> </p>
<!-- TODO lose the @next! --> <!-- TODO lose the @next! -->
<pre>npm init svelte@next</pre> <pre>npm init svelte@next</pre>
<p> <p>
The page you're looking at is purely static HTML, with no client-side interactivity needed. The page you're looking at is purely static HTML, with no client-side interactivity needed. Because of that, we
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening don't need to load any JavaScript. Try viewing the page's source, or opening the devtools network panel and
the devtools network panel and reloading. reloading.
</p> </p>
<p> <p>
The <a href="/todos">TODOs</a> page illustrates SvelteKit's data loading and form handling. Try using The <a href="/todos">TODOs</a> page illustrates SvelteKit's data loading and form handling. Try using it with JavaScript
it with JavaScript disabled! disabled!
</p> </p>
</div> </div>
<style> <style>
.content { .content {
width: 100%; width: 100%;
max-width: var(--column-width); max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto; margin: var(--column-margin-top) auto 0 auto;
} }
</style> </style>

View File

@ -1,59 +1,59 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
export const prerender = true; export const prerender = true;
</script> </script>
<script lang="ts"> <script lang="ts">
import Counter from '$lib/Counter/index.svelte'; import Counter from '$lib/Counter/index.svelte';
</script> </script>
<svelte:head> <svelte:head>
<title>Home</title> <title>Home</title>
</svelte:head> </svelte:head>
<section> <section>
<h1> <h1>
<div class="welcome"> <div class="welcome">
<picture> <picture>
<source srcset="svelte-welcome.webp" type="image/webp" /> <source srcset="svelte-welcome.webp" type="image/webp" />
<img src="svelte-welcome.png" alt="Welcome" /> <img src="svelte-welcome.png" alt="Welcome" />
</picture> </picture>
</div> </div>
to your new<br />SvelteKit app to your new<br />SvelteKit app
</h1> </h1>
<h2> <h2>
try editing <strong>src/routes/index.svelte</strong> try editing <strong>src/routes/index.svelte</strong>
</h2> </h2>
<Counter /> <Counter />
</section> </section>
<style> <style>
section { section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex: 1; flex: 1;
} }
h1 { h1 {
width: 100%; width: 100%;
} }
.welcome { .welcome {
position: relative; position: relative;
width: 100%; width: 100%;
height: 0; height: 0;
padding: 0 0 calc(100% * 495 / 2048) 0; padding: 0 0 calc(100% * 495 / 2048) 0;
} }
.welcome img { .welcome img {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
display: block; display: block;
} }
</style> </style>

View File

@ -4,13 +4,13 @@ import type { Locals } from '$lib/types';
// PATCH /todos/:uid.json // PATCH /todos/:uid.json
export const patch: RequestHandler<Locals, FormData> = async (request) => { export const patch: RequestHandler<Locals, FormData> = async (request) => {
return api(request, `todos/${request.locals.userid}/${request.params.uid}`, { return api(request, `todos/${request.locals.userid}/${request.params.uid}`, {
text: request.body.get('text'), text: request.body.get('text'),
done: request.body.has('done') ? !!request.body.get('done') : undefined done: request.body.has('done') ? !!request.body.get('done') : undefined,
}); });
}; };
// DELETE /todos/:uid.json // DELETE /todos/:uid.json
export const del: RequestHandler<Locals> = async (request) => { export const del: RequestHandler<Locals> = async (request) => {
return api(request, `todos/${request.locals.userid}/${request.params.uid}`); return api(request, `todos/${request.locals.userid}/${request.params.uid}`);
}; };

View File

@ -15,34 +15,34 @@ import type { Locals } from '$lib/types';
const base = 'https://api.svelte.dev'; const base = 'https://api.svelte.dev';
export async function api(request: Request<Locals>, resource: string, data?: {}) { export async function api(request: Request<Locals>, resource: string, data?: {}) {
// user must have a cookie set // user must have a cookie set
if (!request.locals.userid) { if (!request.locals.userid) {
return { status: 401 }; return { status: 401 };
} }
const res = await fetch(`${base}/${resource}`, { const res = await fetch(`${base}/${resource}`, {
method: request.method, method: request.method,
headers: { headers: {
'content-type': 'application/json' 'content-type': 'application/json',
}, },
body: data && JSON.stringify(data) body: data && JSON.stringify(data),
}); });
// if the request came from a <form> submission, the browser's default // if the request came from a <form> submission, the browser's default
// behaviour is to show the URL corresponding to the form's "action" // behaviour is to show the URL corresponding to the form's "action"
// attribute. in those cases, we want to redirect them back to the // attribute. in those cases, we want to redirect them back to the
// /todos page, rather than showing the response // /todos page, rather than showing the response
if (res.ok && request.method !== 'GET' && request.headers.accept !== 'application/json') { if (res.ok && request.method !== 'GET' && request.headers.accept !== 'application/json') {
return { return {
status: 303, status: 303,
headers: { headers: {
location: '/todos' location: '/todos',
} },
}; };
} }
return { return {
status: res.status, status: res.status,
body: await res.json() body: await res.json(),
}; };
} }

View File

@ -4,27 +4,27 @@ import type { Locals } from '$lib/types';
// GET /todos.json // GET /todos.json
export const get: RequestHandler<Locals> = async (request) => { export const get: RequestHandler<Locals> = async (request) => {
// request.locals.userid comes from src/hooks.js // request.locals.userid comes from src/hooks.js
const response = await api(request, `todos/${request.locals.userid}`); const response = await api(request, `todos/${request.locals.userid}`);
if (response.status === 404) { if (response.status === 404) {
// user hasn't created a todo list. // user hasn't created a todo list.
// start with an empty array // start with an empty array
return { body: [] }; return { body: [] };
} }
return response; return response;
}; };
// POST /todos.json // POST /todos.json
export const post: RequestHandler<Locals, FormData> = async (request) => { export const post: RequestHandler<Locals, FormData> = async (request) => {
const response = await api(request, `todos/${request.locals.userid}`, { const response = await api(request, `todos/${request.locals.userid}`, {
// because index.svelte posts a FormData object, // because index.svelte posts a FormData object,
// request.body is _also_ a (readonly) FormData // request.body is _also_ a (readonly) FormData
// object, which allows us to get form data // object, which allows us to get form data
// with the `body.get(key)` method // with the `body.get(key)` method
text: request.body.get('text') text: request.body.get('text'),
}); });
return response; return response;
}; };

View File

@ -1,227 +1,222 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import { enhance } from '$lib/form'; import { enhance } from '$lib/form';
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
// see https://kit.svelte.dev/docs#loading // see https://kit.svelte.dev/docs#loading
export const load: Load = async ({ fetch }) => { export const load: Load = async ({ fetch }) => {
const res = await fetch('/todos.json'); const res = await fetch('/todos.json');
if (res.ok) { if (res.ok) {
const todos = await res.json(); const todos = await res.json();
return { return {
props: { todos } props: { todos },
}; };
} }
const { message } = await res.json(); const { message } = await res.json();
return { return {
error: new Error(message) error: new Error(message),
}; };
}; };
</script> </script>
<script lang="ts"> <script lang="ts">
import { scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
type Todo = { type Todo = {
uid: string; uid: string;
created_at: Date; created_at: Date;
text: string; text: string;
done: boolean; done: boolean;
}; };
export let todos: Todo[]; export let todos: Todo[];
async function patch(res: Response) { async function patch(res: Response) {
const todo = await res.json(); const todo = await res.json();
todos = todos.map((t) => { todos = todos.map((t) => {
if (t.uid === todo.uid) return todo; if (t.uid === todo.uid) return todo;
return t; return t;
}); });
} }
</script> </script>
<svelte:head> <svelte:head>
<title>Todos</title> <title>Todos</title>
</svelte:head> </svelte:head>
<div class="todos"> <div class="todos">
<h1>Todos</h1> <h1>Todos</h1>
<form <form
class="new" class="new"
action="/todos.json" action="/todos.json"
method="post" method="post"
use:enhance={{ use:enhance={{
result: async (res, form) => { result: async (res, form) => {
const created = await res.json(); const created = await res.json();
todos = [...todos, created]; todos = [...todos, created];
form.reset(); form.reset();
} },
}} }}
> >
<input name="text" aria-label="Add todo" placeholder="+ tap to add a todo" /> <input name="text" aria-label="Add todo" placeholder="+ tap to add a todo" />
</form> </form>
{#each todos as todo (todo.uid)} {#each todos as todo (todo.uid)}
<div <div class="todo" class:done={todo.done} transition:scale|local={{ start: 0.7 }} animate:flip={{ duration: 200 }}>
class="todo" <form
class:done={todo.done} action="/todos/{todo.uid}.json?_method=patch"
transition:scale|local={{ start: 0.7 }} method="post"
animate:flip={{ duration: 200 }} use:enhance={{
> pending: (data) => {
<form todo.done = !!data.get('done');
action="/todos/{todo.uid}.json?_method=patch" },
method="post" result: patch,
use:enhance={{ }}
pending: (data) => { >
todo.done = !!data.get('done'); <input type="hidden" name="done" value={todo.done ? '' : 'true'} />
}, <button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
result: patch </form>
}}
>
<input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form>
<form <form
class="text" class="text"
action="/todos/{todo.uid}.json?_method=patch" action="/todos/{todo.uid}.json?_method=patch"
method="post" method="post"
use:enhance={{ use:enhance={{
result: patch result: patch,
}} }}
> >
<input aria-label="Edit todo" type="text" name="text" value={todo.text} /> <input aria-label="Edit todo" type="text" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" /> <button class="save" aria-label="Save todo" />
</form> </form>
<form <form
action="/todos/{todo.uid}.json?_method=delete" action="/todos/{todo.uid}.json?_method=delete"
method="post" method="post"
use:enhance={{ use:enhance={{
result: () => { result: () => {
todos = todos.filter((t) => t.uid !== todo.uid); todos = todos.filter((t) => t.uid !== todo.uid);
} },
}} }}
> >
<button class="delete" aria-label="Delete todo" /> <button class="delete" aria-label="Delete todo" />
</form> </form>
</div> </div>
{/each} {/each}
</div> </div>
<style> <style>
.todos { .todos {
width: 100%; width: 100%;
max-width: var(--column-width); max-width: var(--column-width);
margin: var(--column-margin-top) auto 0 auto; margin: var(--column-margin-top) auto 0 auto;
line-height: 1; line-height: 1;
} }
.new { .new {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
} }
input { input {
border: 1px solid transparent; border: 1px solid transparent;
} }
input:focus-visible { input:focus-visible {
box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1); box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #ff3e00 !important; border: 1px solid #ff3e00 !important;
outline: none; outline: none;
} }
.new input { .new input {
font-size: 28px; font-size: 28px;
width: 100%; width: 100%;
padding: 0.5em 1em 0.3em 1em; padding: 0.5em 1em 0.3em 1em;
box-sizing: border-box; box-sizing: border-box;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
} }
.todo { .todo {
display: grid; display: grid;
grid-template-columns: 2rem 1fr 2rem; grid-template-columns: 2rem 1fr 2rem;
grid-gap: 0.5rem; grid-gap: 0.5rem;
align-items: center; align-items: center;
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
padding: 0.5rem; padding: 0.5rem;
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1)); filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
transform: translate(-1px, -1px); transform: translate(-1px, -1px);
transition: filter 0.2s, transform 0.2s; transition: filter 0.2s, transform 0.2s;
} }
.done { .done {
transform: none; transform: none;
opacity: 0.4; opacity: 0.4;
filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0px 0px 1px rgba(0, 0, 0, 0.1));
} }
form.text { form.text {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1; flex: 1;
} }
.todo input { .todo input {
flex: 1; flex: 1;
padding: 0.5em 2em 0.5em 0.8em; padding: 0.5em 2em 0.5em 0.8em;
border-radius: 3px; border-radius: 3px;
} }
.todo button { .todo button {
width: 2em; width: 2em;
height: 2em; height: 2em;
border: none; border: none;
background-color: transparent; background-color: transparent;
background-position: 50% 50%; background-position: 50% 50%;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
button.toggle { button.toggle {
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 50%; border-radius: 50%;
box-sizing: border-box; box-sizing: border-box;
background-size: 1em auto; background-size: 1em auto;
} }
.done .toggle { .done .toggle {
background-image: url("data:image/svg+xml,%3Csvg width='22' height='16' viewBox='0 0 22 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 1.5L7.4375 14.5L1.5 8.5909' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg width='22' height='16' viewBox='0 0 22 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 1.5L7.4375 14.5L1.5 8.5909' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
} }
.delete { .delete {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 5V22H19.5V5H4.5Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M14 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M2 5H22' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8 5L9.6445 2H14.3885L16 5H8Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E%0A"); background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.5 5V22H19.5V5H4.5Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M14 10V16.5' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M2 5H22' stroke='%23676778' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8 5L9.6445 2H14.3885L16 5H8Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
opacity: 0.2; opacity: 0.2;
} }
.delete:hover, .delete:hover,
.delete:focus { .delete:focus {
transition: opacity 0.2s; transition: opacity 0.2s;
opacity: 1; opacity: 1;
} }
.save { .save {
position: absolute; position: absolute;
right: 0; right: 0;
opacity: 0; opacity: 0;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A"); background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.5 2H3.5C2.67158 2 2 2.67157 2 3.5V20.5C2 21.3284 2.67158 22 3.5 22H20.5C21.3284 22 22 21.3284 22 20.5V3.5C22 2.67157 21.3284 2 20.5 2Z' fill='%23676778' stroke='%23676778' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M17 2V11H7.5V2H17Z' fill='white' stroke='white' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M13.5 5.5V7.5' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M5.99844 2H18.4992' stroke='%23676778' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E%0A");
} }
.todo input:focus + .save, .todo input:focus + .save,
.save:focus { .save:focus {
transition: opacity 0.2s; transition: opacity 0.2s;
opacity: 1; opacity: 1;
} }
</style> </style>

View File

@ -4,26 +4,22 @@ import { windi } from 'svelte-windicss-preprocess';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://github.com/sveltejs/svelte-preprocess // Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors // for more information about preprocessors
preprocess: [windi(), preprocess({ postcss: false })], preprocess: [windi(), preprocess({ postcss: false })],
kit: { kit: {
// hydrate the <div id="svelte"> element in src/app.html // hydrate the <div id="svelte"> element in src/app.html
target: '#svelte', target: '#svelte',
ssr: false, ssr: false,
trailingSlash: 'never', trailingSlash: 'never',
adapter: adapter({ adapter: adapter({
// default options are shown // default options are shown
pages: 'dist', pages: 'dist',
assets: 'dist', assets: 'dist',
fallback: '200.html' fallback: '200.html',
}) }),
} },
}; };
export default config; export default config;
// Workaround until SvelteKit uses Vite 2.3.8 (and it's confirmed to fix the Tailwind JIT problem)
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
process.env.TAILWIND_MODE = dev ? 'watch' : 'build';

View File

@ -1,30 +1,30 @@
{ {
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node", "moduleResolution": "node",
"module": "es2020", "module": "es2020",
"lib": ["es2020"], "lib": ["es2020"],
"target": "es2019", "target": "es2019",
/** /**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types. to enforce using \`import type\` instead of \`import\` for Types.
*/ */
"importsNotUsedAsValues": "error", "importsNotUsedAsValues": "error",
"isolatedModules": true, "isolatedModules": true,
"resolveJsonModule": true, "resolveJsonModule": true,
/** /**
To have warnings/errors of the Svelte compiler at the correct position, To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default. enable source maps by default.
*/ */
"sourceMap": true, "sourceMap": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"baseUrl": ".", "baseUrl": ".",
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"paths": { "paths": {
"$lib/*": ["src/lib/*"] "$lib/*": ["src/lib/*"]
} }
}, },
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
} }