refactor: 🎨 Updating and trying out barbon components

This commit is contained in:
Bradley Shellnut 2022-04-18 19:43:54 -07:00
parent 8c0778f1fa
commit d2da1297f3
19 changed files with 1422 additions and 251 deletions

View file

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

View file

@ -12,12 +12,15 @@
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.21.0",
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"@types/cookie": "^0.5.0", "@types/cookie": "^0.5.0",
"@types/node": "^17.0.24", "@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.19.0", "@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.19.0", "@typescript-eslint/parser": "^5.20.0",
"carbon-components-svelte": "^0.63.0",
"carbon-icons-svelte": "^11.0.1",
"eslint": "^8.13.0", "eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^3.4.1", "eslint-plugin-svelte3": "^3.4.1",

10
playwright.config.ts Normal file
View file

@ -0,0 +1,10 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 3000
}
};
export default config;

File diff suppressed because it is too large Load diff

View file

@ -20,12 +20,10 @@ 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 {
@ -36,11 +34,9 @@ body::before {
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;
} }
@ -104,4 +100,4 @@ button:focus:not(:focus-visible) {
h1 { h1 {
font-size: 2.4rem; font-size: 2.4rem;
} }
} }

15
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 {}
}

1
src/global.d.ts vendored
View file

@ -1 +0,0 @@
/// <reference types="@sveltejs/kit" />

View file

@ -50,6 +50,7 @@
justify-content: center; justify-content: center;
border: 0; border: 0;
background-color: transparent; background-color: transparent;
touch-action: manipulation;
color: var(--text-color); color: var(--text-color);
font-size: 2rem; font-size: 2rem;
} }

View file

@ -1,3 +1,5 @@
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
export function enhance( export function enhance(
@ -7,43 +9,65 @@ export function enhance(
error, error,
result result
}: { }: {
pending?: (data: FormData, form: HTMLFormElement) => void; pending?: ({ data, form }: { data: FormData; form: HTMLFormElement }) => void;
error?: (res: Response | null, error: Error | null, form: HTMLFormElement) => void; error?: ({
result: (res: Response, form: HTMLFormElement) => void; data,
} form,
): { destroy: () => void } { response,
error
}: {
data: FormData;
form: HTMLFormElement;
response: Response | null;
error: Error | null;
}) => void;
result?: ({
data,
form,
response
}: {
data: FormData;
response: Response;
form: HTMLFormElement;
}) => void;
} = {}
) {
let current_token: unknown; let current_token: unknown;
async function handle_submit(e: Event) { async function handle_submit(e: SubmitEvent) {
const token = (current_token = {}); const token = (current_token = {});
e.preventDefault(); e.preventDefault();
const body = new FormData(form); const data = new FormData(form);
if (pending) pending(body, form); if (pending) pending({ data, form });
try { try {
const res = 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 body: data
}); });
if (token !== current_token) return; if (token !== current_token) return;
if (res.ok) { if (response.ok) {
result(res, form); if (result) result({ data, form, response });
const url = new URL(form.action);
url.search = url.hash = '';
invalidate(url.href);
} else if (error) { } else if (error) {
error(res, null, form); error({ data, form, error: null, response });
} else { } else {
console.error(await res.text()); console.error(await response.text());
} }
} catch (e: any) { } catch (e: unknown) {
if (error) { if (error && e instanceof Error) {
error(null, e, form); error({ data, form, error: e, response: null });
} else { } else {
throw e; throw e;
} }

View file

@ -1,6 +1,14 @@
<script lang="ts"> <script lang="ts">
import {
Theme,
RadioButtonGroup,
RadioButton,
} from "carbon-components-svelte";
import type { CarbonTheme } from "carbon-components-svelte/types/Theme/Theme.svelte";
import { page } from '$app/stores'; import { page } from '$app/stores';
import logo from './svelte-logo.svg'; import logo from './svelte-logo.svg';
let theme: CarbonTheme = "white";
</script> </script>
<header> <header>
@ -11,9 +19,6 @@
</div> </div>
<nav> <nav>
<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" />
</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'}>
@ -23,14 +28,18 @@
<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"> <Theme
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" /> render="toggle"
</svg> toggle={{
themes: ['white','g100'],
hideLabel: true,
size: 'sm'
}}
bind:theme
persist
persistKey="__carbon-theme"
/>
</nav> </nav>
<div class="corner">
<!-- TODO put something else here? github link? -->
</div>
</header> </header>
<style> <style>

7
src/lib/types.d.ts vendored
View file

@ -1,7 +0,0 @@
/**
* Can be made globally available by placing this
* inside `global.d.ts` and removing `export` keyword
*/
export interface Locals {
userid: string;
}

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/header/Header.svelte'; import Header from '$lib/header/Header.svelte';
import "carbon-components-svelte/css/all.css";
import '../app.css'; import '../app.css';
</script> </script>

View file

@ -1,18 +0,0 @@
import { api } from './_api';
import type { RequestHandler } from '@sveltejs/kit';
import type { Locals } from '$lib/types';
// PATCH /todos/:uid.json
export const patch: RequestHandler<Locals> = async (event) => {
const data = await event.request.formData();
return api(event, `todos/${event.locals.userid}/${event.params.uid}`, {
text: data.get('text'),
done: data.has('done') ? !!data.get('done') : undefined
});
};
// DELETE /todos/:uid.json
export const del: RequestHandler<Locals> = async (event) => {
return api(event, `todos/${event.locals.userid}/${event.params.uid}`);
};

View file

@ -1,56 +1,22 @@
import type { EndpointOutput, RequestEvent } from '@sveltejs/kit';
import type { Locals } from '$lib/types';
/* /*
This module is used by the /todos.json and /todos/[uid].json This module is used by the /todos endpoint to
endpoints to make calls to api.svelte.dev, which stores todos make calls to api.svelte.dev, which stores todos
for each user. The leading underscore indicates that this is for each user. The leading underscore indicates that this is
a private module, _not_ an endpoint visiting /todos/_api a private module, _not_ an endpoint visiting /todos/_api
will net you a 404 response. will net you a 404 response.
(The data on the todo app will expire periodically; no (The data on the todo app will expire periodically; no
guarantees are made. Don't use it to organise your life.) guarantees are made. Don't use it to organize your life.)
*/ */
const base = 'https://api.svelte.dev'; const base = 'https://api.svelte.dev';
export async function api( export function api(method: string, resource: string, data?: Record<string, unknown>) {
event: RequestEvent<Locals>, return fetch(`${base}/${resource}`, {
resource: string, method,
data?: Record<string, unknown>
): Promise<EndpointOutput> {
// user must have a cookie set
if (!event.locals.userid) {
return { status: 401 };
}
const res = await fetch(`${base}/${resource}`, {
method: event.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
// behaviour is to show the URL corresponding to the form's "action"
// attribute. in those cases, we want to redirect them back to the
// /todos page, rather than showing the response
if (
res.ok &&
event.request.method !== 'GET' &&
event.request.headers.get('accept') !== 'application/json'
) {
return {
status: 303,
headers: {
location: '/todos'
}
};
}
return {
status: res.status,
body: await res.json()
};
} }

View file

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

View file

@ -1,28 +1,5 @@
<script context="module" lang="ts">
import { enhance } from '$lib/form';
import type { Load } from '@sveltejs/kit';
// see https://kit.svelte.dev/docs#loading
export const load: Load = async ({ fetch }) => {
const res = await fetch('/todos.json');
if (res.ok) {
const todos = await res.json();
return {
props: { todos }
};
}
const { message } = await res.json();
return {
error: new Error(message)
};
};
</script>
<script lang="ts"> <script lang="ts">
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';
@ -35,15 +12,6 @@
}; };
export let todos: Todo[]; export let todos: Todo[];
async function patch(res: Response) {
const todo = await res.json();
todos = todos.map((t) => {
if (t.uid === todo.uid) return todo;
return t;
});
}
</script> </script>
<svelte:head> <svelte:head>
@ -55,13 +23,10 @@
<form <form
class="new" class="new"
action="/todos.json" action="/todos"
method="post" method="post"
use:enhance={{ use:enhance={{
result: async (res, form) => { result: async ({ form }) => {
const created = await res.json();
todos = [...todos, created];
form.reset(); form.reset();
} }
}} }}
@ -77,41 +42,33 @@
animate:flip={{ duration: 200 }} animate:flip={{ duration: 200 }}
> >
<form <form
action="/todos/{todo.uid}.json?_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');
}, }
result: patch
}} }}
> >
<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 <form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
class="text" <input type="hidden" name="uid" value={todo.uid} />
action="/todos/{todo.uid}.json?_method=PATCH"
method="post"
use:enhance={{
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?_method=DELETE"
method="post" method="post"
use:enhance={{ use:enhance={{
pending: () => (todo.pending_delete = true), pending: () => (todo.pending_delete = true)
result: () => {
todos = todos.filter((t) => t.uid !== 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>
@ -167,7 +124,7 @@
.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(0 0 1px rgba(0, 0, 0, 0.1));
} }
form.text { form.text {

67
src/routes/todos/index.ts Normal file
View file

@ -0,0 +1,67 @@
import { api } from './_api';
import type { RequestHandler } from './index';
export const get: RequestHandler = async ({ locals }) => {
// locals.userid comes from src/hooks.js
const response = await api('get', `todos/${locals.userid}`);
if (response.status === 404) {
// user hasn't created a todo list.
// start with an empty array
return {
body: {
todos: []
}
};
}
if (response.status === 200) {
return {
body: {
todos: await response.json()
}
};
}
return {
status: response.status
};
};
export const post: RequestHandler = async ({ request, locals }) => {
const form = await request.formData();
await api('post', `todos/${locals.userid}`, {
text: form.get('text')
});
return {};
};
// If the user has JavaScript disabled, the URL will change to
// include the method override unless we redirect back to /todos
const redirect = {
status: 303,
headers: {
location: '/todos'
}
};
export const patch: RequestHandler = async ({ request, locals }) => {
const form = await request.formData();
await api('patch', `todos/${locals.userid}/${form.get('uid')}`, {
text: form.has('text') ? form.get('text') : undefined,
done: form.has('done') ? !!form.get('done') : undefined
});
return redirect;
};
export const del: RequestHandler = async ({ request, locals }) => {
const form = await request.formData();
await api('delete', `todos/${locals.userid}/${form.get('uid')}`);
return redirect;
};

6
tests/test.ts Normal file
View file

@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('about page has expected h1', async ({ page }) => {
await page.goto('/about');
expect(await page.textContent('h1')).toBe('About this app');
});

View file

@ -1,11 +1,20 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "allowJs": true,
"paths": { "checkJs": true,
"$root/*": [ "esModuleInterop": true,
"./src/*" "forceConsistentCasingInFileNames": true,
] "lib": [
} "es2020",
"DOM"
],
"moduleResolution": "node",
"module": "es2020",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es2020",
} }
} }