Files
template-app/modules/notes/ui/pages.tsx
T

84 lines
3.5 KiB
TypeScript

// The notes module's pages, as the one server component the web shell mounts under
// `/<org>/<workspace>/<project>/notes` (apps/web's module mount reads it from `modules/index.ts`):
//
// /notes the list and the form that adds a note
// /notes/<noteId> one note: edit it, attach files, delete it
//
// Every read is a procedure through the same caller the API uses (spec §6.2: one API path); every
// write is a client component that calls a procedure through the shell's transport.
import { type Container, notFound, type TransportCaller } from "@conusai-kit/core"
import type { I18n } from "@conusai-kit/i18n"
import type { ProjectPage } from "@conusai-kit/ui"
import type { ReactElement } from "react"
import { ATTACHMENT_CONTENT_TYPES, noteGet, noteList } from "../contract/index.js"
import { FeedbackButton } from "../guides/feedback-button.js" // kit:guides
import { feedbackLabels } from "../guides/feedback-labels.js" // kit:guides
import { Attachments } from "./attachments.js"
import { DeleteNote } from "./delete-note.js"
import { attachmentLabels, createFormLabels, deleteLabels, editFormLabels, listLabels, sizeLabel } from "./labels.js"
import { NoteForm } from "./note-form.js"
import { NoteList } from "./note-list.js"
export type NotesPage = ProjectPage<TransportCaller, I18n, Container>
export const notesPage: NotesPage = async ({ slugs, segments, caller, i18n }): Promise<ReactElement> => {
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`notes.${key}`, values)
const failed = i18n.message("app.failed")
const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/notes`
if (segments.length === 0) {
const { notes } = await caller.call(noteList, slugs)
return (
<>
<h1>{t("title")}</h1>
<p>{t("intro")}</p>
<FeedbackButton labels={feedbackLabels(t)} /* kit:guides */ />
{/* guide: The form that adds a note */}
<div data-guide="notes.new-note">
<NoteForm slugs={slugs} target={{ mode: "create", listHref }} labels={createFormLabels(t, failed)} />
</div>
<NoteList
notes={notes}
hrefFor={(noteId) => `${listHref}/${noteId}`}
labels={listLabels(t)}
formatDate={(iso) => i18n.date(new Date(iso))}
/>
</>
)
}
const [noteId, ...rest] = segments
if (noteId === undefined || rest.length > 0) throw notFound("no such page")
const { note, attachments, attachmentsEnabled } = await caller.call(noteGet, { ...slugs, noteId })
return (
<>
<p>
<a href={listHref}>{t("detail.back")}</a>
</p>
<h1>{note.title}</h1>
<p>{t("detail.changed", { date: i18n.date(new Date(note.updatedAt)) })}</p>
<NoteForm
slugs={slugs}
target={{ mode: "edit", noteId: note.noteId, version: note.version, title: note.title, body: note.body }}
labels={editFormLabels(t, failed)}
/>
<Attachments
slugs={slugs}
noteId={note.noteId}
rows={attachments.map((attachment) => ({
attachmentId: attachment.attachmentId,
fileId: attachment.fileId,
filename: attachment.filename,
contentType: attachment.contentType,
size: sizeLabel(t, attachment.size),
}))}
enabled={attachmentsEnabled}
accept={ATTACHMENT_CONTENT_TYPES}
labels={attachmentLabels(t, failed, attachments.length)}
/>
<DeleteNote slugs={slugs} noteId={note.noteId} listHref={listHref} labels={deleteLabels(t, failed)} />
</>
)
}