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,5 +1,5 @@
{
"useTabs": true,
"useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100

View file

@ -12,12 +12,15 @@
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
},
"devDependencies": {
"@playwright/test": "^1.21.0",
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"@types/cookie": "^0.5.0",
"@types/node": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"carbon-components-svelte": "^0.63.0",
"carbon-icons-svelte": "^11.0.1",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0",
"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;
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%
);
background: linear-gradient(180deg,
var(--primary-color) 0%,
var(--secondary-color) 10.45%,
var(--tertiary-color) 41.35%);
}
body::before {
@ -36,11 +34,9 @@ body::before {
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%
);
background: radial-gradient(50% 50% at 50% 50%,
var(--pure-white) 0%,
rgba(255, 255, 255, 0) 100%);
opacity: 0.05;
}

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;
border: 0;
background-color: transparent;
touch-action: manipulation;
color: var(--text-color);
font-size: 2rem;
}

View file

@ -1,3 +1,5 @@
import { invalidate } from '$app/navigation';
// this action (https://svelte.dev/tutorial/actions) allows us to
// progressively enhance a <form> that already works without JS
export function enhance(
@ -7,43 +9,65 @@ export function enhance(
error,
result
}: {
pending?: (data: FormData, form: HTMLFormElement) => void;
error?: (res: Response | null, error: Error | null, form: HTMLFormElement) => void;
result: (res: Response, form: HTMLFormElement) => void;
}
): { destroy: () => void } {
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;
} = {}
) {
let current_token: unknown;
async function handle_submit(e: Event) {
async function handle_submit(e: SubmitEvent) {
const token = (current_token = {});
e.preventDefault();
const body = new FormData(form);
const data = new FormData(form);
if (pending) pending(body, form);
if (pending) pending({ data, form });
try {
const res = await fetch(form.action, {
const response = await fetch(form.action, {
method: form.method,
headers: {
accept: 'application/json'
},
body
body: data
});
if (token !== current_token) return;
if (res.ok) {
result(res, form);
if (response.ok) {
if (result) result({ data, form, response });
const url = new URL(form.action);
url.search = url.hash = '';
invalidate(url.href);
} else if (error) {
error(res, null, form);
error({ data, form, error: null, response });
} else {
console.error(await res.text());
console.error(await response.text());
}
} catch (e: any) {
if (error) {
error(null, e, form);
} catch (e: unknown) {
if (error && e instanceof Error) {
error({ data, form, error: e, response: null });
} else {
throw e;
}

View file

@ -1,6 +1,14 @@
<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 logo from './svelte-logo.svg';
let theme: CarbonTheme = "white";
</script>
<header>
@ -11,9 +19,6 @@
</div>
<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>
<li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
<li class:active={$page.url.pathname === '/about'}>
@ -23,14 +28,18 @@
<a sveltekit:prefetch href="/todos">Todos</a>
</li>
</ul>
<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" />
</svg>
<Theme
render="toggle"
toggle={{
themes: ['white','g100'],
hideLabel: true,
size: 'sm'
}}
bind:theme
persist
persistKey="__carbon-theme"
/>
</nav>
<div class="corner">
<!-- TODO put something else here? github link? -->
</div>
</header>
<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">
import Header from '$lib/header/Header.svelte';
import "carbon-components-svelte/css/all.css";
import '../app.css';
</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
endpoints to make calls to api.svelte.dev, which stores todos
This module is used by the /todos endpoint to
make calls to api.svelte.dev, which stores todos
for each user. The leading underscore indicates that this is
a private module, _not_ an endpoint visiting /todos/_api
will net you a 404 response.
(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';
export async function api(
event: RequestEvent<Locals>,
resource: string,
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,
export function api(method: string, resource: string, data?: Record<string, unknown>) {
return fetch(`${base}/${resource}`, {
method,
headers: {
'content-type': 'application/json'
},
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">
import { enhance } from '$lib/form';
import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
@ -35,15 +12,6 @@
};
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>
<svelte:head>
@ -55,13 +23,10 @@
<form
class="new"
action="/todos.json"
action="/todos"
method="post"
use:enhance={{
result: async (res, form) => {
const created = await res.json();
todos = [...todos, created];
result: async ({ form }) => {
form.reset();
}
}}
@ -77,41 +42,33 @@
animate:flip={{ duration: 200 }}
>
<form
action="/todos/{todo.uid}.json?_method=PATCH"
action="/todos?_method=PATCH"
method="post"
use:enhance={{
pending: (data) => {
pending: ({ data }) => {
todo.done = !!data.get('done');
},
result: patch
}
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<input type="hidden" name="done" value={todo.done ? '' : 'true'} />
<button class="toggle" aria-label="Mark todo as {todo.done ? 'not done' : 'done'}" />
</form>
<form
class="text"
action="/todos/{todo.uid}.json?_method=PATCH"
method="post"
use:enhance={{
result: patch
}}
>
<form class="text" action="/todos?_method=PATCH" method="post" use:enhance>
<input type="hidden" name="uid" value={todo.uid} />
<input aria-label="Edit todo" type="text" name="text" value={todo.text} />
<button class="save" aria-label="Save todo" />
</form>
<form
action="/todos/{todo.uid}.json?_method=DELETE"
action="/todos?_method=DELETE"
method="post"
use:enhance={{
pending: () => (todo.pending_delete = true),
result: () => {
todos = todos.filter((t) => t.uid !== todo.uid);
}
pending: () => (todo.pending_delete = true)
}}
>
<input type="hidden" name="uid" value={todo.uid} />
<button class="delete" aria-label="Delete todo" disabled={todo.pending_delete} />
</form>
</div>
@ -167,7 +124,7 @@
.done {
transform: none;
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 {

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",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"$root/*": [
"./src/*"
]
}
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"es2020",
"DOM"
],
"moduleResolution": "node",
"module": "es2020",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es2020",
}
}