* add pb models * api rules has updated to view public * pb setup is completed * add migrate script * add pb migrations * ignore storage * add pocketbase SDK dependency * change Tag.id and PageCardDto.id to string for PocketBase * add DocV1 content block types * add PB client singleton and PB record types * rewrite factory for PocketBase record types * rewrite api.ts for PocketBase SDK * remove unused Strapi files, add PB to config * mark Task 7 complete in plan * feat: verify build, tests, and lint pass with PocketBase API layer * move completed plan: 2026-04-06-pocketbase-api-layer.md * scaffold hudozka-writer with Vite 8, React 19, TS 5.9, Tiptap * add PocketBase client and DocV1 types to writer * add Login component for PocketBase superuser auth * add App shell with auth gate and page loader * add DocV1 <-> Tiptap serialization layer * mark Task 5 complete in writer redesign plan * add custom Tiptap nodes for DocV1 block types * add Tiptap editor with toolbar and save to PocketBase * add image picker with browse and upload * feat: verify writer build and complete integration task * move completed plan: 2026-04-06-hudozka-writer-redesign.md * add local env * rm textblock from writer * add writer editor improvements design spec * add writer editor improvements implementation plan * feat: use @tiptap/markdown for text block serialization * feat: add global markdown/rendered toggle * feat: add BlockMenu component for block type selection * feat: add hover + button for block insertion * feat: add slash commands for block insertion * feat: add interactive file block editor with FilePicker * feat: show resolved page titles in card grid block * move completed plan: 2026-04-07-writer-editor-improvements.md * writer ui improved * pages migration * spec: draft/publish workflow for hudozka-writer * spec: rename draft_doc to draft in spec * plan: writer draft/publish implementation * plan has moved * gitignores * plans upd * feat(writer): update PbPage type for draft/publish workflow * feat(writer): add save/publish/discard handlers with dirty tracking * feat(writer): add DraftBanner component * feat(writer): add Publish button and update toolbar layout * feat(writer): add published toggle and update duplicate for new schema * docs: mark manual verification steps as skipped in writer draft/publish plan * move completed plan: 20260407-writer-draft-publish.md * feat(writer): add BlockActions component for inline block menus * feat(writer): add draggable NodeViews for text blocks * feat(writer): use BlockActions in custom block NodeViews * feat(writer): replace floating BlockInsert with inline BlockActions * fix(writer): ensure all block wrappers have position: relative for BlockActions * chore: mark manual verification tasks as skipped in writer block actions plan * move completed plan: 20260408-writer-block-actions.md * writer update * refactor(writer): simplify BlockActions to always-visible delete + drag handle * feat(writer): add BlockInsert component for inserting blocks between nodes * feat(writer): redesign block sidebar with always-visible actions and insert button * move completed plan: 20260408-writer-block-actions-redesign.md * add spec: textblock container redesign for writer editor * feat(writer): add TextBlock container node definition * feat(writer): replace DraggableXxx wrappers with TextBlock container * feat(writer): update Editor extensions for TextBlock container * feat(writer): add split button to BlockActions for text blocks * feat(writer): update serialization for TextBlock container * feat(writer): update BlockMenu and SlashCommands for TextBlock * fix(writer): use Fragment.from() for replaceWith calls in BlockActions and SlashCommands * move completed plan: 20260408-textblock-container.md * add air
137 lines
2.9 KiB
TypeScript
137 lines
2.9 KiB
TypeScript
export type StrapiMediaObject = {
|
|
url: string,
|
|
name: string,
|
|
hash: string,
|
|
ext: string,
|
|
mime: string,
|
|
width: number,
|
|
height: number,
|
|
size: number,
|
|
}
|
|
|
|
export type StrapiMedia = StrapiMediaObject & {
|
|
id: number,
|
|
alternativeText: string | null,
|
|
caption: string | null,
|
|
formats: null | {
|
|
thumbnail: StrapiMediaObject,
|
|
large: StrapiMediaObject,
|
|
medium: StrapiMediaObject,
|
|
small: StrapiMediaObject,
|
|
},
|
|
}
|
|
|
|
export type StrapiComponentText = {
|
|
__component: "hudozka.text",
|
|
id: number,
|
|
text: string,
|
|
}
|
|
|
|
export type StrapiComponentImage = {
|
|
__component: "hudozka.image",
|
|
id: number,
|
|
wide: boolean,
|
|
caption: string,
|
|
media: StrapiMedia,
|
|
}
|
|
|
|
export type StrapiComponentDocument = {
|
|
__component: "hudozka.document",
|
|
id: number,
|
|
title: string,
|
|
media: StrapiMediaObject & { id: number },
|
|
}
|
|
|
|
export type StrapiComponentEmbed = {
|
|
__component: "hudozka.embed",
|
|
id: number,
|
|
src: string,
|
|
}
|
|
|
|
export type StrapiComponentCardGrid = {
|
|
__component: "hudozka.card-grid",
|
|
id: number,
|
|
items: StrapiPageCard[],
|
|
}
|
|
|
|
export type StrapiComponent =
|
|
| StrapiComponentText
|
|
| StrapiComponentImage
|
|
| StrapiComponentDocument
|
|
| StrapiComponentEmbed
|
|
| StrapiComponentCardGrid
|
|
|
|
export type StrapiTag = {
|
|
id: number,
|
|
slug: string,
|
|
name: string,
|
|
}
|
|
|
|
export type StrapiPage = {
|
|
id: number,
|
|
title: string,
|
|
excerpt: string,
|
|
slug: string,
|
|
date: string | null,
|
|
cover?: StrapiMedia,
|
|
published_at: string,
|
|
created_at: string,
|
|
updated_at: string,
|
|
content: StrapiComponent[],
|
|
tags: StrapiTag[],
|
|
}
|
|
|
|
export type StrapiPageCard = {
|
|
__component: "hudozka.page-card",
|
|
id: number,
|
|
page: StrapiPage,
|
|
layout: "small" | "medium" | "big",
|
|
}
|
|
|
|
export type StrapiHome = {
|
|
id: number,
|
|
title: string,
|
|
cards: StrapiPageCard[],
|
|
}
|
|
|
|
export type StrapiMenu = {
|
|
id: number,
|
|
homeLabel: string,
|
|
menu: StrapiComponentCardGrid,
|
|
}
|
|
|
|
async function get<T>(base: string, path: string): Promise<T> {
|
|
const res = await fetch(`${base}${path}`)
|
|
if (!res.ok) {
|
|
throw new Error(`GET ${path} -> ${res.status}`)
|
|
}
|
|
return await res.json() as T
|
|
}
|
|
|
|
export async function fetchAllPages(base: string): Promise<StrapiPage[]> {
|
|
const limit = 100
|
|
let start = 0
|
|
const all: StrapiPage[] = []
|
|
while (true) {
|
|
const batch = await get<StrapiPage[]>(base, `/pages?_limit=${limit}&_start=${start}`)
|
|
if (batch.length === 0) {
|
|
break
|
|
}
|
|
all.push(...batch)
|
|
start += limit
|
|
}
|
|
return all
|
|
}
|
|
|
|
export async function fetchTags(base: string): Promise<StrapiTag[]> {
|
|
return await get<StrapiTag[]>(base, "/tags")
|
|
}
|
|
|
|
export async function fetchHome(base: string): Promise<StrapiHome> {
|
|
return await get<StrapiHome>(base, "/home")
|
|
}
|
|
|
|
export async function fetchMenu(base: string): Promise<StrapiMenu> {
|
|
return await get<StrapiMenu>(base, "/menu")
|
|
}
|