Skip to content

Commit

Permalink
Add toasts
Browse files Browse the repository at this point in the history
  • Loading branch information
ChiriVulpes committed Dec 30, 2024
1 parent 246618f commit e146624
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 11 deletions.
19 changes: 17 additions & 2 deletions lang/en-nz.quilt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ create: Create!
cancel: Cancel
confirm: Confirm

## toast
failed-to-save: Failed to save {0}
saved: Saved {0}

## form
name/label: Name
vanity/label: Vanity
Expand Down Expand Up @@ -148,6 +152,9 @@ pronouns/hint: The pronouns that others should refer to you with. These will app
action/logout: Log Out
action/delete: Delete Account

toast/failed-to-save=shared/toast/failed-to-save | account data
toast/saved=shared/toast/saved | account data

## account/auth
service/accessibility/connect: connect {0}
service/accessibility/disconnect: disconnect {0} account {1}
Expand Down Expand Up @@ -189,7 +196,11 @@ action/label/delete=shared/action/delete

## work-edit

### shared/form
### shared
toast/failed-to-save=shared/toast/failed-to-save
toast/saved=shared/toast/saved

#### form
name/label=shared/form/name/label
name/hint: The name of your work. This does not have to be unique.
vanity/label=shared/form/vanity/label
Expand Down Expand Up @@ -224,7 +235,11 @@ action/label/delete=shared/action/delete

## chapter-edit

### shared/form
### shared
toast/failed-to-save=shared/toast/failed-to-save | chapter
toast/saved=shared/toast/saved | chapter

#### form
name/label=shared/form/name/label
name/hint: The name of this chapter. This does not have to be unique.
body/label=shared/form/body/label
Expand Down
2 changes: 2 additions & 0 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Session from "model/Session"
import Navigator from "navigation/Navigate"
import style from "style"
import Component from "ui/Component"
import ToastList from "ui/component/core/toast/ToastList"
import Masthead from "ui/component/Masthead"
import InputBus from "ui/InputBus"
import FocusListener from "ui/utility/FocusListener"
Expand Down Expand Up @@ -93,6 +94,7 @@ async function App (): Promise<App> {
const app: App = Component()
.style("app")
.append(masthead, masthead.sidebar, content)
.append(ToastList())
.extend<AppExtensions>(app => ({
navigate: Navigator(app),
view,
Expand Down
1 change: 0 additions & 1 deletion src/navigation/Navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ function Navigator (app: App): Navigator {
window.addEventListener("popstate", navigate.fromURL)

Object.assign(window, { navigate })

return navigate
}

Expand Down
9 changes: 3 additions & 6 deletions src/ui/component/core/Button.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import Component from "ui/Component"
import type { ComponentName } from "ui/utility/StyleManipulator"
import type { ComponentNameType } from "ui/utility/StyleManipulator"
import type { UnsubscribeState } from "utility/State"
import State from "utility/State"

type ButtonType = keyof { [KEY in ComponentName as KEY extends `button-type-${infer TYPE}--${string}` ? TYPE
: KEY extends `button-type-${infer TYPE}` ? TYPE
: never]: string[] }

type ButtonIcon = keyof { [KEY in ComponentName as KEY extends `button-icon-${infer TYPE}` ? TYPE : never]: string[] }
type ButtonType = ComponentNameType<"button-type">
type ButtonIcon = ComponentNameType<"button-icon">

interface ButtonTypeManipulator<HOST> {
(...buttonTypes: ButtonType[]): HOST
Expand Down
42 changes: 42 additions & 0 deletions src/ui/component/core/toast/Toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ErrorResponse } from "api.fluff4.me"
import Component from "ui/Component"
import type { ToastComponent } from "ui/component/core/toast/ToastList"
import type { Quilt } from "ui/utility/StringApplicator"

interface Toast<PARAMS extends any[] = []> {
duration: number
initialise: (toast: ToastComponent, ...params: PARAMS) => any
}

function Toast<PARAMS extends any[]> (toast: Toast<PARAMS>) {
return toast
}

export default Toast

export const TOAST_SUCCESS = Toast({
duration: 2000,
initialise (toast, translation: Quilt.SimpleKey | Quilt.Handler) {
toast.title.text.use(translation)
},
})

function isErrorResponse (error: Error): error is ErrorResponse {
return (error as ErrorResponse).headers !== undefined
}

export const TOAST_ERROR = Toast({
duration: 5000,
initialise (toast, translation: Quilt.SimpleKey | Quilt.Handler, error: Error) {
toast.title.text.use(translation)
if (!isErrorResponse(error) || !error.detail)
toast.content.text.set(error.message)
else
toast.content
.append(Component()
.style("toast-error-type")
.text.set(error.message))
.text.append(": ")
.text.append(error.detail)
},
})
120 changes: 120 additions & 0 deletions src/ui/component/core/toast/ToastList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Component from "ui/Component"
import type Toast from "ui/component/core/toast/Toast"
import type { ComponentNameType } from "ui/utility/StyleManipulator"
import Async from "utility/Async"
import Task from "utility/Task"
import Time from "utility/Time"

declare global {
export const toast: ToastList
}

////////////////////////////////////
//#region Toast Component

interface ToastTypeManipulator<HOST> {
(...toastTypes: ToastType[]): HOST
remove (...toastTypes: ToastType[]): HOST
}

interface ToastExtensions {
readonly title: Component
readonly content: Component
readonly type: ToastTypeManipulator<this>
}

export interface ToastComponent extends Component, ToastExtensions { }

type ToastType = ComponentNameType<"toast-type">

const ToastComponent = Component.Builder((component): ToastComponent => {
const title = Component()
.style("toast-title")
.appendTo(component)

return component
.style("toast")
.extend<ToastExtensions>(toast => ({
title,
content: undefined!,
type: Object.assign(
(...types: ToastType[]) => {
for (const type of types)
toast.style(`toast-type-${type}`)
return toast
},
{
remove (...types: ToastType[]) {
for (const type of types)
toast.style.remove(`toast-type-${type}`)
return toast
},
},
),
}))
.extendJIT("content", toast => Component()
.style("toast-content")
.appendTo(toast))
}).setName("Toast")

//#endregion
////////////////////////////////////

////////////////////////////////////
//#region Toast List

interface ToastListExtensions extends Record<ToastType, <PARAMS extends any[]>(toast: Toast<PARAMS>, ...params: PARAMS) => ToastComponent> {

}

interface ToastList extends Component, ToastListExtensions { }

const ToastList = Component.Builder((component): ToastList => {
const toasts: ToastList = component
.style("toast-list")
.extend<ToastListExtensions>(toasts => ({
info: add.bind(null, "info"),
success: add.bind(null, "success"),
warning: add.bind(null, "warning"),
}))

Object.assign(window, { toast: toasts })
return toasts

function add<PARAMS extends any[]> (type: ToastType, toast: Toast<PARAMS>, ...params: PARAMS) {
const component = ToastComponent()
.type(type)
.style("toast--measuring")
.tweak(toast.initialise, ...params)

void lifecycle(toast, component)

return component
}

async function lifecycle (toast: Toast<any[]>, component: ToastComponent) {
const wrapper = Component().style("toast-wrapper").appendTo(toasts)
component.style("toast--measuring").appendTo(wrapper)
await Task.yield()
const rect = component.rect.value
component.style.remove("toast--measuring")
wrapper.style.setProperty("height", `${rect.height}px`)

await Async.sleep(toast.duration)

component.style("toast--hide")
wrapper.style.removeProperties("height")
await Promise.race([
new Promise<any>(resolve => component.event.subscribe("animationend", resolve)),
Async.sleep(Time.seconds(1)),
])

return
wrapper.remove()
}
})

//#endregion
////////////////////////////////////

export default ToastList
3 changes: 3 additions & 0 deletions src/ui/utility/StyleManipulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type State from "utility/State"
import type { UnsubscribeState } from "utility/State"

export type ComponentName = keyof typeof style
export type ComponentNameType<PREFIX extends string> = keyof { [KEY in ComponentName as KEY extends `${PREFIX}-${infer TYPE}--${string}` ? TYPE
: KEY extends `${PREFIX}-${infer TYPE}` ? TYPE
: never]: string[] }

interface StyleManipulatorFunctions<HOST> {
get (): ComponentName[]
Expand Down
3 changes: 3 additions & 0 deletions src/ui/view/account/AccountViewForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import LabelledTable from "ui/component/core/LabelledTable"
import LabelledTextInputBlock from "ui/component/core/LabelledTextInputBlock"
import TextEditor from "ui/component/core/TextEditor"
import TextInput from "ui/component/core/TextInput"
import { TOAST_ERROR, TOAST_SUCCESS } from "ui/component/core/toast/Toast"
import VanityInput from "ui/component/VanityInput"

type AccountViewFormType =
Expand Down Expand Up @@ -93,10 +94,12 @@ export default Component.Builder((component, type: AccountViewFormType) => {
})

if (response instanceof Error) {
toast.warning(TOAST_ERROR, "view/account/toast/failed-to-save", response)
console.error(response)
return
}

toast.success(TOAST_SUCCESS, "view/account/toast/saved")
Session.setAuthor(response.data)
})

Expand Down
3 changes: 3 additions & 0 deletions src/ui/view/chapter/ChapterEditForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Form from "ui/component/core/Form"
import LabelledTable from "ui/component/core/LabelledTable"
import TextEditor from "ui/component/core/TextEditor"
import TextInput from "ui/component/core/TextInput"
import { TOAST_ERROR, TOAST_SUCCESS } from "ui/component/core/toast/Toast"
import type State from "utility/State"

export default Component.Builder((component, state: State<Chapter | undefined>, workParams: WorkParams) => {
Expand Down Expand Up @@ -82,10 +83,12 @@ export default Component.Builder((component, state: State<Chapter | undefined>,
})()

if (response instanceof Error) {
toast.warning(TOAST_ERROR, "view/chapter-edit/shared/toast/failed-to-save", response)
console.error(response)
return
}

toast.success(TOAST_SUCCESS, "view/chapter-edit/shared/toast/saved")
state.value = response?.data
})

Expand Down
9 changes: 7 additions & 2 deletions src/ui/view/work/WorkEditForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LabelledTable from "ui/component/core/LabelledTable"
import Textarea from "ui/component/core/Textarea"
import TextEditor from "ui/component/core/TextEditor"
import TextInput from "ui/component/core/TextInput"
import { TOAST_ERROR, TOAST_SUCCESS } from "ui/component/core/toast/Toast"
import type State from "utility/State"

export default Component.Builder((component, state: State<WorkFull | undefined>) => {
Expand Down Expand Up @@ -64,12 +65,14 @@ export default Component.Builder((component, state: State<WorkFull | undefined>)
form.event.subscribe("submit", async event => {
event.preventDefault()

const name = nameInput.value

const response = await (() => {
switch (type) {
case "create":
return EndpointWorkCreate.query({
body: {
name: nameInput.value,
name,
vanity: vanityInput.value,
description: descriptionInput.value,
synopsis: synopsisInput.useMarkdown(),
Expand All @@ -90,7 +93,7 @@ export default Component.Builder((component, state: State<WorkFull | undefined>)
vanity: state.value.vanity,
},
body: {
name: nameInput.value,
name,
vanity: vanityInput.value,
description: descriptionInput.value,
synopsis: synopsisInput.useMarkdown(),
Expand All @@ -101,10 +104,12 @@ export default Component.Builder((component, state: State<WorkFull | undefined>)
})()

if (response instanceof Error) {
toast.warning(TOAST_ERROR, quilt => quilt["view/work-edit/shared/toast/failed-to-save"](name), response)
console.error(response)
return
}

toast.success(TOAST_SUCCESS, quilt => quilt["view/work-edit/shared/toast/saved"](name))
state.value = response?.data
})

Expand Down
Loading

0 comments on commit e146624

Please sign in to comment.