diff --git a/lang/en-nz.quilt b/lang/en-nz.quilt index b18c32d..f9fc4b3 100644 --- a/lang/en-nz.quilt +++ b/lang/en-nz.quilt @@ -107,6 +107,10 @@ popover/account: Account Settings # work chapters/label=shared/term/chapters +description/empty: This work has no description. + +# author +description/empty: This author prefers to remain elusive and mysterious... # view container/alt: main content @@ -199,6 +203,7 @@ action/delete: Delete Work ### chapters title=shared/term/chapters +content/empty: This work does not contain any chapters. action/label/new: New Chapter action/label/view=shared/action/view action/label/love=shared/action/love diff --git a/src/ui/component/Author.ts b/src/ui/component/Author.ts index 0198759..2932356 100644 --- a/src/ui/component/Author.ts +++ b/src/ui/component/Author.ts @@ -1,6 +1,7 @@ import type { AuthorFull } from "api.fluff4.me" import Component from "ui/Component" import Block from "ui/component/core/Block" +import Slot from "ui/component/core/Slot" export default Component.Builder((component, author: AuthorFull) => { component @@ -17,7 +18,12 @@ export default Component.Builder((component, author: AuthorFull) => { Component() .style("author-description") - .setMarkdownContent(author.description.body) + .append(Slot.using(author.description.body, (slot, body) => { + if (body) + slot.setMarkdownContent(body) + else + slot.style("placeholder").text.use("author/description/empty") + })) .appendTo(block.content) return block diff --git a/src/ui/component/Work.ts b/src/ui/component/Work.ts index 1e07051..13dfc2a 100644 --- a/src/ui/component/Work.ts +++ b/src/ui/component/Work.ts @@ -1,10 +1,11 @@ -import type { Author, Work as WorkData } from "api.fluff4.me" +import type { Author, Work as WorkData, WorkFull } from "api.fluff4.me" import Session from "model/Session" import Tags from "model/Tags" import Component from "ui/Component" import Block from "ui/component/core/Block" import Button from "ui/component/core/Button" import Link from "ui/component/core/Link" +import Slot from "ui/component/core/Slot" import TextLabel from "ui/component/core/TextLabel" import Timestamp from "ui/component/core/Timestamp" import Tag from "ui/component/Tag" @@ -15,7 +16,9 @@ interface WorkExtensions { interface Work extends Block, WorkExtensions { } -const Work = Component.Builder(async (component, work: WorkData, author?: Author): Promise => { +const Work = Component.Builder(async (component, work: WorkData & Partial, author?: Author): Promise => { + author = author ?? work.synopsis?.mentions[0] + component .viewTransition("work") .style("work") @@ -38,11 +41,39 @@ const Work = Component.Builder(async (component, work: WorkData, author?: Author .text.set(author.name)) block.content.style("work-content") - Component() - .style("work-description") - .setMarkdownContent(work.description) + + Slot() + .use(isFlush, (slot, isFlush) => { + const shouldShowDescription = isFlush || (work.synopsis?.body && work.description) + if (shouldShowDescription) + Component() + .style("work-description") + .style.toggle(!work.description, "placeholder") + .tweak(component => { + if (work.description) + component.text.set(work.description) + else + component.text.use("work/description/empty") + }) + .appendTo(slot) + + if (!isFlush) + Component() + .style("work-synopsis") + .style.toggle(!work.synopsis?.body && !work.description, "placeholder") + .append(Slot.using(work.synopsis ?? work.description, (slot, synopsis) => { + if (typeof synopsis === "string") + slot.text.set(synopsis) + else if (!synopsis.body) + slot.text.use("work/description/empty") + else + slot.setMarkdownContent(synopsis.body) + })) + .appendTo(slot) + }) .appendTo(block.content) + let tagsWrapper: Component | undefined const tags = await Tags.resolve(work.global_tags) for (const tag of tags) { diff --git a/src/ui/component/core/Slot.ts b/src/ui/component/core/Slot.ts index 342beb7..294f5c3 100644 --- a/src/ui/component/core/Slot.ts +++ b/src/ui/component/core/Slot.ts @@ -1,6 +1,6 @@ import Component from "ui/Component" -import type State from "utility/State" import type { UnsubscribeState } from "utility/State" +import State from "utility/State" export type SlotCleanup = () => any @@ -63,18 +63,8 @@ const Slot = Object.assign( .event.subscribe("remove", () => cleanup?.()) }), { - // Using: Component.Builder( (slot: Component, state: State, contentSupplier: (value: T) => Component | undefined): Slot => { - // return slot.and(Slot) - // .use(state, (slot, value) => { - // contentSupplier(value)?.appendTo(slot) - // }) - // }), - // If: Component.Builder((slot: Component, state: State, contentSupplier: () => Component | undefined): Slot => { - // return slot.and(Slot) - // .if(state, (slot) => { - // contentSupplier()?.appendTo(slot) - // }) - // }), + using: (value: T | State, initialiser: (slot: Slot, value: T) => SlotCleanup | Component | void) => + Slot().use(State.get(value), initialiser), } ) diff --git a/src/ui/view/WorkView.ts b/src/ui/view/WorkView.ts index 2ff4df6..a824029 100644 --- a/src/ui/view/WorkView.ts +++ b/src/ui/view/WorkView.ts @@ -2,6 +2,7 @@ import EndpointChapterGetAll from "endpoint/chapter/EndpointChapterGetAll" import type { WorkParams } from "endpoint/work/EndpointWorkGet" import EndpointWorkGet from "endpoint/work/EndpointWorkGet" import Session from "model/Session" +import Component from "ui/Component" import Chapter from "ui/component/Chapter" import Button from "ui/component/core/Button" import Paginator from "ui/component/core/Paginator" @@ -51,6 +52,10 @@ export default ViewDefinition({ Chapter(chapterData, workData, authorData) .appendTo(slot) }) + paginator.orElse(slot => Component() + .style("placeholder") + .text.use("view/work/chapters/content/empty") + .appendTo(slot)) return view }, diff --git a/src/ui/view/work/WorkEditForm.ts b/src/ui/view/work/WorkEditForm.ts index d119488..dab70b1 100644 --- a/src/ui/view/work/WorkEditForm.ts +++ b/src/ui/view/work/WorkEditForm.ts @@ -43,6 +43,7 @@ export default Component.Builder((component, state: State) .content((content, label) => content.append(vanityInput.setLabel(label))) const descriptionInput = Textarea() + .default.bind(state.map(component, work => work?.description)) table.label(label => label.text.use("view/work-edit/shared/form/description/label")) .content((content, label) => content.append(descriptionInput.setLabel(label))) @@ -82,7 +83,8 @@ export default Component.Builder((component, state: State) body: { name: nameInput.value, vanity: vanityInput.value, - description: synopsisInput.useMarkdown(), + description: descriptionInput.value, + synopsis: synopsisInput.useMarkdown(), }, }) } diff --git a/src/utility/State.ts b/src/utility/State.ts index 6fa8229..922d633 100644 --- a/src/utility/State.ts +++ b/src/utility/State.ts @@ -8,6 +8,7 @@ export type StateOr = State | T export type UnsubscribeState = () => void interface ReadableState { + readonly isState: true readonly value: T readonly equals: (value: V) => boolean @@ -49,6 +50,7 @@ interface InternalState { function State (defaultValue: T, equals?: (a: T, b: T) => boolean): State { const result: Mutable> & InternalState = { + isState: true, [SYMBOL_VALUE]: defaultValue, [SYMBOL_SUBSCRIBERS]: [], get value () { @@ -151,6 +153,14 @@ function State (defaultValue: T, equals?: (a: T, b: T) => boolean): State namespace State { + export function is (value: unknown): value is ReadableState { + return typeof value === "object" && (value as ReadableState)?.isState === true + } + + export function get (value: T | State): ReadableState { + return is(value) ? value : State(value) + } + export interface Generator extends ReadableState { refresh (): this observe (owner: Component, ...states: ReadableState[]): this