[examples] Update SvelteKit example with latest boilerplate (#7892)

Replaces https://github.com/vercel/vercel/pull/7674 with the latest SvelteKit boilerplate.
This commit is contained in:
Lee Robinson
2022-05-30 02:57:46 -05:00
committed by GitHub
parent b07ff7431f
commit c414288b2f
22 changed files with 2362 additions and 1900 deletions

View File

@@ -7,3 +7,4 @@ node_modules
.env.* .env.*
!.env.example !.env.example
.vercel .vercel
.output

View File

@@ -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

View File

@@ -0,0 +1,6 @@
{
"useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}

View File

@@ -14,31 +14,29 @@ If you're seeing this, you've probably already done this step. Congrats!
```bash ```bash
# create a new project in the current directory # create a new project in the current directory
npm init svelte@next npm init svelte
# create a new project in my-app # create a new project in my-app
npm init svelte@next my-app npm init svelte my-app
``` ```
> Note: the `@next` is temporary
## Developing ## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: Once you've created a project and installed dependencies with `pnpm install`, start a development server:
```bash ```bash
npm run dev pnpm run dev
# or start the server and open the app in a new browser tab # or start the server and open the app in a new browser tab
npm run dev -- --open pnpm run dev -- --open
``` ```
## Building ## Building
This uses the adapter-auto for SvelteKit, which detects Vercel and runs adapter-vercel on your behalf. To create a production version of your app:
```bash ```bash
npm run build pnpm run build
``` ```
> 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. You can preview the production build with `npm run preview`.

View File

@@ -1,10 +1,13 @@
{ {
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "allowJs": true,
"paths": { "checkJs": true,
"$lib": ["src/lib"], "esModuleInterop": true,
"$lib/*": ["src/lib/*"] "forceConsistentCasingInFileNames": true,
} "resolveJsonModule": true,
}, "skipLibCheck": true,
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] "sourceMap": true,
"strict": true
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,29 @@
{ {
"private": true, "private": true,
"name": "sveltekit",
"version": "0.0.1",
"scripts": { "scripts": {
"dev": "svelte-kit dev", "dev": "svelte-kit dev",
"build": "svelte-kit build", "build": "svelte-kit build",
"package": "svelte-kit package", "package": "svelte-kit package",
"preview": "svelte-kit preview" "preview": "svelte-kit preview",
"prepare": "svelte-kit sync",
"check": "svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-check --tsconfig ./jsconfig.json --watch",
"lint": "prettier --check --plugin-search-dir=. .",
"format": "prettier --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"svelte": "^3.46.0" "@types/cookie": "^0.4.1",
"prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.5.0",
"svelte": "^3.46.0",
"svelte-check": "^2.2.6",
"typescript": "~4.6.2"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.0", "@fontsource/fira-mono": "^4.5.0",
"@lukeed/uuid": "^2.0.0",
"cookie": "^0.4.1" "cookie": "^0.4.1"
} }
} }

1633
examples/sveltekit/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

15
examples/sveltekit/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
interface Locals {
userid: string;
}
// interface Platform {}
// interface Session {}
// interface Stuff {}
}

View File

@@ -1,13 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" content="Svelte demo app" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="icon" href="%svelte.assets%/favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> %sveltekit.head%
%svelte.head% </head>
</head> <body>
<body> <div>%sveltekit.body%</div>
<div>%svelte.body%</div> </body>
</body>
</html> </html>

View File

@@ -1,23 +1,23 @@
import cookie from 'cookie'; import * as cookie from 'cookie';
import { v4 as uuid } from '@lukeed/uuid';
/** @type {import('@sveltejs/kit').Handle} */
export const handle = async ({ event, resolve }) => { export const handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || ''); const cookies = cookie.parse(event.request.headers.get('cookie') || '');
event.locals.userid = cookies.userid || uuid(); event.locals.userid = cookies['userid'] || crypto.randomUUID();
const response = await resolve(event); const response = await resolve(event);
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( response.headers.set(
'set-cookie', 'set-cookie',
cookie.serialize('userid', event.locals.userid, { cookie.serialize('userid', event.locals.userid, {
path: '/', path: '/',
httpOnly: true httpOnly: true
}) })
); );
} }
return response; return response;
}; };

View File

@@ -1,102 +1,107 @@
<script> <script>
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, m) { /**
// handle negative numbers * @param {number} n
return ((n % m) + m) % m; * @param {number} m
} */
function modulo(n, m) {
// handle negative numbers
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 class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong> <strong class="hidden" 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); touch-action: manipulation;
font-size: 2rem; color: var(--text-color);
} 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: flex; display: flex;
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;
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%;
} }
.hidden { .hidden {
top: -100%; top: -100%;
user-select: none; user-select: none;
} }
</style> </style>

View File

@@ -2,54 +2,81 @@ import { invalidate } from '$app/navigation';
// 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
/**
* @param {HTMLFormElement} form
* @param {{
* pending?: ({ data, form }: { data: FormData; form: HTMLFormElement }) => void;
* error?: ({
* data,
* form,
* response,
* error
* }: {
* data: FormData;
* form: HTMLFormElement;
* response: Response | null;
* error: Error | null;
* }) => void;
* result?: ({
* data,
* form,
* response
* }: {
* data: FormData;
* response: Response;
* form: HTMLFormElement;
* }) => void;
* }} [opts]
*/
export function enhance(form, { pending, error, result } = {}) { export function enhance(form, { pending, error, result } = {}) {
let current_token; let current_token;
async function handle_submit(e) { /** @param {SubmitEvent} e */
const token = (current_token = {}); async function handle_submit(e) {
const token = (current_token = {});
e.preventDefault(); e.preventDefault();
const data = new FormData(form); const data = new FormData(form);
if (pending) pending({ data, form }); if (pending) pending({ data, form });
try { try {
const response = await fetch(form.action, { const response = await fetch(form.action, {
method: form.method, method: form.method,
headers: { headers: {
accept: 'application/json' accept: 'application/json'
}, },
body: data body: data
}); });
if (token !== current_token) return; if (token !== current_token) return;
if (response.ok) { if (response.ok) {
if (result) result({ data, form, response }); if (result) result({ data, form, response });
const url = new URL(form.action); const url = new URL(form.action);
url.search = url.hash = ''; url.search = url.hash = '';
invalidate(url.href); invalidate(url.href);
} else if (error) { } else if (error) {
error({ data, form, error: null, response }); error({ data, form, error: null, response });
} else { } else {
console.error(await response.text()); console.error(await response.text());
} }
} catch (e) { } catch (e) {
if (error) { if (error && e instanceof Error) {
error({ data, form, error: e, response: null }); error({ data, form, error: e, response: null });
} 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

@@ -1,124 +1,124 @@
<script> <script>
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.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li> <li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.url.pathname === '/about'}> <li class:active={$page.url.pathname === '/about'}>
<a sveltekit:prefetch href="/about">About</a> <a sveltekit:prefetch href="/about">About</a>
</li> </li>
<li class:active={$page.url.pathname === '/todos'}> <li class:active={$page.url.pathname === '/todos'}>
<a sveltekit:prefetch href="/todos">Todos</a> <a sveltekit:prefetch href="/todos">Todos</a>
</li> </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: 0.1em; letter-spacing: 0.1em;
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,45 +1,45 @@
<script> <script>
import Header from '$lib/header/Header.svelte'; import Header from '$lib/header/Header.svelte';
import '../app.css'; import '../app.css';
</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> <style>
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>

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>
<meta name="description" content="About this app" />
</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 command line and following the prompts: following into your command line and following the prompts:
</p> </p>
<!-- TODO lose the @next! --> <pre>npm init svelte</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 don't need to load any JavaScript. Try viewing the page's source, or opening Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
the devtools network panel and reloading. the devtools network panel and 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 disabled! it with JavaScript 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,60 @@
<script context="module"> <script context="module">
export const prerender = true; export const prerender = true;
</script> </script>
<script> <script>
import Counter from '$lib/Counter.svelte'; import Counter from '$lib/Counter.svelte';
</script> </script>
<svelte:head> <svelte:head>
<title>Home</title> <title>Home</title>
<meta name="description" content="Svelte demo app" />
</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

@@ -11,12 +11,17 @@
const base = 'https://api.svelte.dev'; const base = 'https://api.svelte.dev';
/**
* @param {string} method
* @param {string} resource
* @param {Record<string, unknown>} [data]
*/
export function api(method, resource, data) { export function api(method, resource, data) {
return fetch(`${base}/${resource}`, { return fetch(`${base}/${resource}`, {
method, method,
headers: { headers: {
'content-type': 'application/json' 'content-type': 'application/json'
}, },
body: data && JSON.stringify(data) body: data && JSON.stringify(data)
}); });
} }

View File

@@ -1,66 +1,70 @@
import { api } from './_api'; import { api } from './_api';
/** @type {import('./__types').RequestHandler} */
export const get = async ({ locals }) => { export const get = async ({ locals }) => {
// locals.userid comes from src/hooks.js // locals.userid comes from src/hooks.js
const response = await api('get', `todos/${locals.userid}`); const response = await api('get', `todos/${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 { return {
body: { body: {
todos: [] todos: []
} }
}; };
} }
if (response.status === 200) { if (response.status === 200) {
return { return {
body: { body: {
todos: await response.json() todos: await response.json()
} }
}; };
} }
return { return {
status: response.status status: response.status
}; };
}; };
/** @type {import('./index').RequestHandler} */
export const post = async ({ request, locals }) => { export const post = async ({ request, locals }) => {
const form = await request.formData(); const form = await request.formData();
await api('post', `todos/${locals.userid}`, { await api('post', `todos/${locals.userid}`, {
text: form.get('text') text: form.get('text')
}); });
return {}; return {};
}; };
// If the user has JavaScript disabled, the URL will change to // If the user has JavaScript disabled, the URL will change to
// include the method override unless we redirect back to /todos // include the method override unless we redirect back to /todos
const redirect = { const redirect = {
status: 303, status: 303,
headers: { headers: {
location: '/todos' location: '/todos'
} }
}; };
/** @type {import('./index').RequestHandler} */
export const patch = async ({ request, locals }) => { export const patch = async ({ request, locals }) => {
const form = await request.formData(); const form = await request.formData();
await api('patch', `todos/${locals.userid}/${form.get('uid')}`, { await api('patch', `todos/${locals.userid}/${form.get('uid')}`, {
text: form.has('text') ? form.get('text') : undefined, text: form.has('text') ? form.get('text') : undefined,
done: form.has('done') ? !!form.get('done') : undefined done: form.has('done') ? !!form.get('done') : undefined
}); });
return redirect; return redirect;
}; };
/** @type {import('./index').RequestHandler} */
export const del = async ({ request, locals }) => { export const del = async ({ request, locals }) => {
const form = await request.formData(); const form = await request.formData();
await api('delete', `todos/${locals.userid}/${form.get('uid')}`); await api('delete', `todos/${locals.userid}/${form.get('uid')}`);
return redirect; return redirect;
}; };

View File

@@ -1,178 +1,190 @@
<script> <script>
import { enhance } from '$lib/form'; import { enhance } from '$lib/form';
import { scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
export let todos; /**
* @typedef {{
* uid: string;
* created_at: Date;
* text: string;
* done: boolean;
* pending_delete: boolean;
* }} Todo
*/
/** @type {Todo[]} */
export let todos;
</script> </script>
<svelte:head> <svelte:head>
<title>Todos</title> <title>Todos</title>
<meta name="description" content="A todo list app" />
</svelte:head> </svelte:head>
<div class="todos"> <div class="todos">
<h1>Todos</h1> <h1>Todos</h1>
<form <form
class="new" class="new"
action="/todos" action="/todos"
method="post" method="post"
use:enhance={{ use:enhance={{
result: async ({ form }) => { result: async ({ form }) => {
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="todo"
class:done={todo.done} class:done={todo.done}
transition:scale|local={{ start: 0.7 }} transition:scale|local={{ start: 0.7 }}
animate:flip={{ duration: 200 }} animate:flip={{ duration: 200 }}
> >
<form <form
action="/todos?_method=PATCH" action="/todos?_method=PATCH"
method="post" method="post"
use:enhance={{ use:enhance={{
pending: ({ data }) => { pending: ({ data }) => {
todo.done = !!data.get('done'); todo.done = !!data.get('done');
} }
}} }}
> >
<input type="hidden" name="uid" value={todo.uid} /> <input type="hidden" name="uid" value={todo.uid} />
<input type="hidden" name="done" value={todo.done ? '' : 'true'} /> <input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" /> <button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form> </form>
<form class="text" action="/todos?_method=PATCH" method="post" use:enhance> <form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
<input type="hidden" name="uid" value={todo.uid} /> <input type="hidden" name="uid" value={todo.uid} />
<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?_method=DELETE" action="/todos?_method=DELETE"
method="post" method="post"
use:enhance={{ use:enhance={{
pending: () => (todo.pending_delete = true) pending: () => (todo.pending_delete = true)
}} }}
> >
<input type="hidden" name="uid" value={todo.uid} /> <input type="hidden" name="uid" value={todo.uid} />
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} /> <button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
</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(0 0 1px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 0 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

@@ -2,14 +2,14 @@ import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
kit: { kit: {
adapter: adapter(), adapter: adapter(),
// Override http methods in the Todo forms // Override http methods in the Todo forms
methodOverride: { methodOverride: {
allowed: ['PATCH', 'DELETE'] allowed: ['PATCH', 'DELETE']
} }
} }
}; };
export default config; export default config;