Skip to content

Commit

Permalink
utilize more types in LFNext (#1664)
Browse files Browse the repository at this point in the history
  • Loading branch information
billy clark authored Jan 10, 2023
1 parent 6c1766b commit f8f611f
Show file tree
Hide file tree
Showing 29 changed files with 644 additions and 602 deletions.
4 changes: 1 addition & 3 deletions next-app/README-Tech.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,11 @@ Of course choosing tailwindcss comes with a number of criticisms but we believe
1. The codebase can be kept free of class clutter by simply utlizing `@apply` in the `<style>` section of our `.svelte` files but when doing this a couple of things need to be taken into consideration:
> modifiers are not supported in `@apply` without additional `preprocess` and `postcss` configuration and increased specificity can sometimes cause problems when class overrides are needed on an element directly.
> modifiers are not supported in `@apply` without additional `preprocess` and `postcss` configuration and increased specificity can sometimes cause problems when class overrides are needed on an element directly. See [Warn: Unused CSS selector](https://github.com/saadeghi/daisyui/discussions/1490) for some elaboration. We do still use the `@apply` in a component but not scoped, simply for an organizational benefit, see `/next-app/src/lib/forms/Form.svelte`
2. We will inevitably have situations where parent components or views will want to pass styles down to children. This is very common and requires writing global classes anyway. Using something like tailwindcss or bootstrap takes all of that work off of our plate. Unused CSS will still get purged by the compiler so the app doesn't take on unnecessary bloat.
3. daisyUI is built upon tailwindcss anyway so we benefit from using it in both situations.
[ ] Confirm no additional configuration is required to purge unused CSS.
#### daisyUI
With such a small team, we were always going to need a well thought-out UI library to keep us from having to make a bunch of design decisions along the way. While there are a handful of Svelte-based libraries, none of them met the criteria necessary to help make our team as efficient as possible. There were many mature UI systems already in place built upon tailwindcss and daisyUI represents one of the best of the ones we looked at in it's goal of further simplifying the use of tailwindcss. Additionally, daisyUI has already integrated many best practices and opinions in its design decisions.
Expand Down
691 changes: 324 additions & 367 deletions next-app/package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions next-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
"clean": "rimraf .svelte-kit build node_modules && echo '🔔 reminder: you will need to reinstall deps, `npm i`'"
},
"devDependencies": {
"@sveltejs/adapter-node": "next",
"@sveltejs/kit": "next",
"@sveltejs/adapter-node": "^1",
"@sveltejs/kit": "^1",
"@tailwindcss/typography": "^0.5.2",
"autoprefixer": "^10",
"daisyui": "^2",
"rimraf": "^3",
"svelte": "^3",
"svelte-check": "^2",
"tailwindcss": "^3.0.23",
"tailwindcss": "^3",
"tslib": "^2",
"typescript": "^4",
"vite": "^4"
Expand Down
4 changes: 2 additions & 2 deletions next-app/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<body data-sveltekit-preload-data=hover>
<div style='display: contents'>%sveltekit.body%</div>
</body>
</html>
2 changes: 1 addition & 1 deletion next-app/src/lib/PageHeader.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<h1 class='text-2xl md:text-3xl { $$props.class }'>
<h1 class='text-2xl md:text-3xl'>
<slot />
</h1>
43 changes: 43 additions & 0 deletions next-app/src/lib/Stats.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang=ts>
import { goto } from '$app/navigation'
type Stat = {
title: string,
value?: number,
icon?: ConstructorOfATypedSvelteComponent,
url?: string | URL,
}
export let stats: Stat[]
$: _stats = stats.map(({ title, value, icon, url }) => ({
title,
value,
icon,
href: value ? url : ''
}))
const clicked = (href: string | URL = '') => href ? goto(href) : {}
</script>

<!-- https://daisyui.com/components/stat/ -->
<dl class='stats shadow max-w-full'> <!-- added max-w-full so a horiz scroll will appear on small screens rather than stretching the whole doc -->
{#each _stats as { title, value, icon, href }}
<div class='stat place-items-center' class:href on:click={ () => clicked(href) } on:keydown={ () => clicked(href) }>
<dt class=stat-title>{ title }</dt>
<dd class='stat-value text-primary'>{ Number(value).toLocaleString() }</dd>

{#if icon}
<div class='stat-figure text-primary pl-4'>
<svelte:component this={icon} />
</div>
{/if}
</div>
{/each}
</dl>

<style>
.href {
cursor: pointer;
}
</style>
22 changes: 4 additions & 18 deletions next-app/src/lib/error/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
import { browser } from '$app/environment'
import { writable, type Writable } from 'svelte/store'

interface LfError {
message: string,
code?: number,
}
export const error: Writable<LfError> = writable({ message: '' })

export function throw_error(message: string, code: number = 0) {
throw set({ message, code })
}
export const error: Writable<Error> = writable(Error())

export const dismiss = set
export const dismiss = () => error.set(Error())

if (browser) {
// https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror#window.addEventListenererror
window.addEventListener('error', (event: ErrorEvent) => set(event.error))
window.addEventListener('error', (event: ErrorEvent) => error.set(event.error))

// https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
window.onunhandledrejection = (event: PromiseRejectionEvent) => set(event.reason)
}

function set({ message, code = 0 }: LfError) {
error.set({ code, message })

return { code, message }
window.onunhandledrejection = (event: PromiseRejectionEvent) => error.set(event.reason)
}
51 changes: 20 additions & 31 deletions next-app/src/lib/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,37 @@
import { throw_error } from '$lib/error'
import { start, stop } from '$lib/progress'
import type { HttpMethod } from '@sveltejs/kit/types/private'

export async function CREATE(url, body) { return await custom_fetch('post' , url, body) }
export async function GET (url ) { return await custom_fetch('get' , url ) }
export async function UPDATE(url, body) { return await custom_fetch('put' , url, body) }
export async function DELETE(url ) { return await custom_fetch('delete', url ) }
type FetchArgs = {
url: string,
body?: object,
}
interface AdaptedFetchArgs extends FetchArgs {
method: HttpMethod,
}

// https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
// export const upload = async formData => await CREATE('post', formData)
export async function CREATE({url, body}: FetchArgs) { return await adapted_fetch({method: 'POST' , url, body}) }
export async function GET ({url }: FetchArgs) { return await adapted_fetch({method: 'GET' , url }) }
export async function UPDATE({url, body}: FetchArgs) { return await adapted_fetch({method: 'PUT' , url, body}) }
export async function DELETE({url }: FetchArgs) { return await adapted_fetch({method: 'DELETE', url }) }

// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Supplying_request_options
async function custom_fetch(method, url, body) {
const headers = {
'content-type': 'application/json',
}

// when dealing with FormData, i.e., when uploading files, allow the browser to set the request up
// so boundary information is built properly.
if (body instanceof FormData) {
delete headers['content-type']
} else {
body = JSON.stringify(body)
}

async function adapted_fetch({method, url, body}: AdaptedFetchArgs) {
start(url)
const response = await fetch(url, {

const response: Response = await fetch(url, {
method,
headers,
body,
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
})
.catch (throw_error) // these only occur for network errors, like these:
// * request made with a bad host, e.g., //httpbin
// * the host is refusing connections
// * client is offline, i.e., airplane mode or something
// * CORS preflight failures
.finally(() => stop(url))

// reminder: fetch does not throw exceptions for non-200 responses (https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch)
if (! response.ok) {
const results = await response.json().catch(() => {}) || {}

const message = results.message || response.statusText

throw_error(message, response.status)
throw Error(results.message || response.statusText)
}

return await response.json()
Expand Down
11 changes: 4 additions & 7 deletions next-app/src/lib/forms/Button.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
<script>
export let danger = false
export {clazz as class}
let clazz = ''
</script>

<!-- https://daisyui.com/components/button -->
<button on:click class:danger class='btn btn-primary { $$props.class }'>
<button on:click class:btn-error={danger} class='btn btn-primary { clazz }'>
<slot />
</button>

<style>
.danger { @apply
btn-error;
}
</style>
2 changes: 1 addition & 1 deletion next-app/src/lib/forms/Form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<slot />
</form>

<style>
<style lang=postcss>
:global(form > input) { @apply
mb-6;
}
Expand Down
8 changes: 4 additions & 4 deletions next-app/src/lib/forms/Input.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script>
<script lang=ts>
import { onMount } from 'svelte'
export let label = ''
Expand All @@ -8,7 +8,7 @@
export let autofocus = false
let id = randomId()
let input = {}
let input: HTMLInputElement
onMount(autofocusIfRequested)
Expand All @@ -17,11 +17,11 @@
}
function autofocusIfRequested() {
autofocus && input.focus()
autofocus && input?.focus()
}
// works around "svelte(invalid-type)" warning, i.e., can't have a dynamic type AND bind:value...keep an eye on https://github.com/sveltejs/svelte/issues/3921
function typeWorkaround(node) {
function typeWorkaround(node: HTMLInputElement) {
node.type = type
}
</script>
Expand Down
13 changes: 7 additions & 6 deletions next-app/src/lib/progress/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { writable } from 'svelte/store'
import { writable, type Writable } from 'svelte/store'

export const loading = writable(false)
export const loading: Writable<boolean> = writable(false)

const pending = []
type Id = string | number
const pending: Id[] = []

export function start(id) {
export function start(id: Id) {
loading.set(true)

pending.push(id)
}

export function stop(id) {
const i = pending.findIndex(anId => anId === id)
export function stop(id: Id) {
const i = pending.findIndex(_id => _id === id)

if (i >= 0) {
pending.splice(i,1)
Expand Down
45 changes: 26 additions & 19 deletions next-app/src/lib/server/sf.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { error } from '@sveltejs/kit'
import type { HttpMethod } from '@sveltejs/kit/types/private'

/**
*
* @typedef RPC
* @type {object}
* @property {string} name Name of the remote procedure to call
* @property {string[]} [args] Arguments to pass to the remote procedure
* @property {string} [cookie] https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
*
* @param { RPC } rpc
*/
export async function sf(rpc) {
const { name, args = [], cookie } = rpc
export type Rpc = {
name: string,
args?: string[] | object[],
cookie?: string,
}

type FetchArgs = {
url: string,
method: HttpMethod,
body: object,
cookie?: string,
}

type SfResponse = {
error?: {
message: string
},
result?: any,
}

export async function sf<T>({name, args = [], cookie = ''}: Rpc): Promise<T> {
const body = {
id: Date.now(),
method: name,
Expand All @@ -21,7 +30,7 @@ export async function sf(rpc) {
},
}

const results = await custom_fetch(`${process.env.API_HOST}/api/sf`, 'post', body, cookie)
const results = await adapted_fetch({url: `${process.env.API_HOST}/api/sf`, method: 'POST', body, cookie})

if (results.error) {
console.log('lib/server/sf.ts.sf results.error: ', {results})
Expand All @@ -36,27 +45,25 @@ export async function sf(rpc) {
return results.result
}

async function custom_fetch(url, method, body, cookie) {
const bodyAsJSON = JSON.stringify(body)

async function adapted_fetch({url, method, body, cookie = ''}: FetchArgs): Promise<SfResponse> {
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Supplying_request_options
const response: Response = await fetch(url, {
method,
headers: {
'content-type': 'application/json',
cookie,
},
body: bodyAsJSON,
body: JSON.stringify(body),
}).catch(e => {
// these only occur for network errors, like these:
// request made with a bad host, e.g., //httpbin
// the host is refusing connections
console.log(`lib/server/sf.ts.custom_fetch caught error on ${url}=>${bodyAsJSON}: `, {e})
console.log(`lib/server/sf.ts.adapted_fetch caught error on ${url}: `, {body}, {e})
throw error(500, 'NETWORK ERROR with legacy app')
})

if (! response.ok) {
console.log(`lib/server/sf.ts.custom_fetch response !ok ${url}=>${bodyAsJSON}: `, await response.text())
console.log(`lib/server/sf.ts.adapted_fetch response !ok ${url}: `, {body}, await response.text())
throw error(response.status, response.statusText)
}

Expand Down
14 changes: 12 additions & 2 deletions next-app/src/lib/server/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { error } from '@sveltejs/kit'
import { sf } from '$lib/server/sf'

export async function fetch_current_user(cookie) {
const { userId, userProjectRole } = await sf({
type LegacySession = {
userId: string,
userProjectRole: string,
}

type User = {
id: string,
role: string,
}

export async function fetch_current_user(cookie: string): Promise<User> {
const { userId, userProjectRole }: LegacySession = await sf({
name: 'session_getSessionData',
cookie,
})
Expand Down
Loading

0 comments on commit f8f611f

Please sign in to comment.