@openrails/contentkit-ui 0.69.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +17 -13
  2. package/dist/client/content/comments.d.ts +2 -2
  3. package/dist/client/content/comments.js +3 -3
  4. package/dist/client/content/comments.js.map +1 -1
  5. package/dist/client/content/polls.d.ts +2 -2
  6. package/dist/client/content/polls.js.map +1 -1
  7. package/dist/client/content/posts.d.ts +20 -12
  8. package/dist/client/content/posts.js +5 -13
  9. package/dist/client/content/posts.js.map +1 -1
  10. package/dist/client/content/reactions.js +4 -4
  11. package/dist/client/content/reactions.js.map +1 -1
  12. package/dist/client/content/types.d.ts +2 -2
  13. package/dist/client/content/types.js +1 -1
  14. package/dist/client/content/types.js.map +1 -1
  15. package/dist/client/generated/error-codes.js +2 -2
  16. package/dist/client/generated/error-codes.js.map +1 -1
  17. package/dist/client/generated/routes.d.ts +120 -172
  18. package/dist/client/generated/routes.js +207 -191
  19. package/dist/client/generated/routes.js.map +1 -1
  20. package/dist/client/generated/wire.d.ts +6 -1
  21. package/dist/client/generated/wire.js.map +1 -1
  22. package/dist/client/index.d.ts +1 -1
  23. package/dist/client/media/client.d.ts +7 -5
  24. package/dist/client/media/client.js +1 -1
  25. package/dist/client/media/client.js.map +1 -1
  26. package/dist/client/media/rules.d.ts +2 -2
  27. package/dist/client/media/rules.js +1 -5
  28. package/dist/client/media/rules.js.map +1 -1
  29. package/dist/client/taxonomy.d.ts +1 -1
  30. package/dist/client/taxonomy.js.map +1 -1
  31. package/dist/client.js +5 -5
  32. package/dist/components/social/poll-editor.d.ts +1 -1
  33. package/dist/components/social/poll-editor.js.map +1 -1
  34. package/dist/react/comments.d.ts +1 -1
  35. package/dist/react/comments.js.map +1 -1
  36. package/dist/react/polls.d.ts +2 -2
  37. package/dist/react/polls.js.map +1 -1
  38. package/dist/react/posts.d.ts +12 -10
  39. package/dist/react/posts.js +132 -99
  40. package/dist/react/posts.js.map +1 -1
  41. package/dist/react/public.d.ts +2 -2
  42. package/dist/react/public.js +7 -4
  43. package/dist/react/public.js.map +1 -1
  44. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\n/** An editor read's upload rules (a viewer read has none). */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? [];\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */\nexport function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined {\n const at = presets.filter((p) => p.kind === kind && ruleFor([{ path: p.from }], path));\n return (name ? at.find((p) => p.name === name) : undefined) ?? at.find((p) => p.aspect) ?? at[0];\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). Empty for a preview preset.\n */\nexport function defaultImage(rule: PresetRule, id: string): PublicPreset {\n const r = ratio(rule.aspect);\n if (rule.first || rule.to.includes(NAME)) return { preset: rule.name, aspect: rule.aspect, renditions: [] };\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w })), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\n/** An editor read's upload rules (a viewer read has none). */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? [];\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */\nexport function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined {\n const at = presets.filter((p) => p.kind === kind && ruleFor([{ path: p.from }], path));\n return (name ? at.find((p) => p.name === name) : undefined) ?? at.find((p) => p.aspect) ?? at[0];\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). null when the preset has no default (`rule.default`).\n */\nexport function defaultImage(rule: PresetRule, id: string): PublicPreset | null {\n const r = ratio(rule.aspect);\n if (!rule.default || rule.first || rule.to.includes(NAME)) return null;\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w })), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { Http } from './http.js';
3
3
  import { RouteQuery } from './route.js';
4
4
  /** GET /taxonomy/nodes filters, as the route names them. */
5
5
  export type NodeQuery = RouteQuery<"GET", "/nodes">;
6
- /** The taxonomy admin API (Taxonomy permission). */
6
+ /** The taxonomy admin API (root:taxonomy:write). */
7
7
  export declare class TaxonomyClient {
8
8
  private readonly http;
9
9
  constructor(http: Http);
@@ -1 +1 @@
1
- {"version":3,"file":"taxonomy.js","names":[],"sources":["../../src/client/taxonomy.ts"],"sourcesContent":["import type {\n Assignment,\n AssignmentsWritten,\n ContentRef,\n Count,\n CountsRebuilt,\n Edge,\n EdgesWritten,\n EffectiveTagsOf,\n MergeReport,\n Name,\n Node,\n NodeDetail,\n NodeInput,\n NodePage,\n NodeUpdate,\n} from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, type RouteQuery } from \"./route.js\";\n\n/** GET /taxonomy/nodes filters, as the route names them. */\nexport type NodeQuery = RouteQuery<\"GET\", \"/nodes\">;\n\n/** The taxonomy admin API (Taxonomy permission). */\nexport class TaxonomyClient {\n constructor(private readonly http: Http) {}\n\n nodes(q: NodeQuery = {}, signal?: AbortSignal): Promise<NodePage> {\n return call(this.http, \"GET\", \"/nodes\", { query: q, signal });\n }\n node(id: string, signal?: AbortSignal): Promise<NodeDetail> {\n return call(this.http, \"GET\", \"/nodes/{id}\", { params: { id }, signal });\n }\n createNodes(nodes: NodeInput[]): Promise<Node[]> {\n return this.write(call(this.http, \"POST\", \"/nodes\", { body: nodes }));\n }\n updateNode(id: string, patch: NodeUpdate): Promise<Node> {\n return this.write(call(this.http, \"PATCH\", \"/nodes/{id}\", { params: { id }, body: patch }));\n }\n /** Marks the node deleted. */\n deleteNode(id: string): Promise<Node> {\n return this.write(call(this.http, \"DELETE\", \"/nodes/{id}\", { params: { id } }));\n }\n /** Replaces, adds or removes names. */\n names(id: string, op: \"set\" | \"add\" | \"remove\", names: Name[]): Promise<NodeDetail> {\n const method = op === \"set\" ? \"PUT\" : op === \"add\" ? \"POST\" : \"DELETE\";\n return this.write(call(this.http, method, \"/nodes/{id}/names\", { params: { id }, body: names }));\n }\n merge(id: string, into: string): Promise<MergeReport> {\n return this.write(call(this.http, \"POST\", \"/nodes/{id}/merge\", { params: { id }, body: { into_taxonomy_id: into } }));\n }\n edges(op: \"add\" | \"remove\", edges: Edge[]): Promise<EdgesWritten> {\n return this.write(call(this.http, op === \"add\" ? \"POST\" : \"DELETE\", \"/edges\", { body: edges }));\n }\n /** Assigns or unassigns nodes; suppressCounts skips count upkeep (rebuildCounts afterwards). */\n assignments(op: \"add\" | \"remove\", assignments: Assignment[], o: { suppressCounts?: boolean } = {}): Promise<AssignmentsWritten> {\n return this.write(call(this.http, op === \"add\" ? \"POST\" : \"DELETE\", \"/assignments\", { body: assignments, query: { suppress_counts: o.suppressCounts } }));\n }\n /** The effective tags (work and version) of each content. */\n effective(refs: ContentRef[], signal?: AbortSignal): Promise<EffectiveTagsOf[]> {\n return call(this.http, \"POST\", \"/effective\", { body: refs, signal });\n }\n /** Content counts of nodes, by taxonomy id. */\n counts(ids: readonly string[], signal?: AbortSignal): Promise<Record<string, Count[]>> {\n return call(this.http, \"GET\", \"/counts\", { query: { taxonomy_id: ids }, signal });\n }\n rebuildCounts(): Promise<CountsRebuilt> {\n return this.write(call(this.http, \"POST\", \"/counts/rebuild\"));\n }\n\n private async write<T>(p: Promise<T>): Promise<T> {\n const emit = this.http.captureChanges();\n const out = await p;\n emit({ type: \"taxonomy.changed\" });\n return out;\n }\n}\n"],"mappings":";;AAwBA,IAAa,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAE1C,MAAM,IAAe,CAAC,GAAG,GAAyC;EAChE,OAAO,EAAK,KAAK,MAAM,OAAO,UAAU;GAAE,OAAO;GAAG;EAAO,CAAC;CAC9D;CACA,KAAK,GAAY,GAA2C;EAC1D,OAAO,EAAK,KAAK,MAAM,OAAO,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG;EAAO,CAAC;CACzE;CACA,YAAY,GAAqC;EAC/C,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,QAAQ,UAAU,EAAE,MAAM,EAAM,CAAC,CAAC;CACtE;CACA,WAAW,GAAY,GAAkC;EACvD,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,SAAS,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM;EAAM,CAAC,CAAC;CAC5F;CAEA,WAAW,GAA2B;EACpC,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,UAAU,eAAe,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC,CAAC;CAChF;CAEA,MAAM,GAAY,GAA8B,GAAoC;EAClF,IAAM,IAAS,MAAO,QAAQ,QAAQ,MAAO,QAAQ,SAAS;EAC9D,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,GAAQ,qBAAqB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM;EAAM,CAAC,CAAC;CACjG;CACA,MAAM,GAAY,GAAoC;EACpD,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,QAAQ,qBAAqB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM,EAAE,kBAAkB,EAAK;EAAE,CAAC,CAAC;CACtH;CACA,MAAM,GAAsB,GAAsC;EAChE,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,MAAO,QAAQ,SAAS,UAAU,UAAU,EAAE,MAAM,EAAM,CAAC,CAAC;CAChG;CAEA,YAAY,GAAsB,GAA2B,IAAkC,CAAC,GAAgC;EAC9H,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,MAAO,QAAQ,SAAS,UAAU,gBAAgB;GAAE,MAAM;GAAa,OAAO,EAAE,iBAAiB,EAAE,eAAe;EAAE,CAAC,CAAC;CAC1J;CAEA,UAAU,GAAoB,GAAkD;EAC9E,OAAO,EAAK,KAAK,MAAM,QAAQ,cAAc;GAAE,MAAM;GAAM;EAAO,CAAC;CACrE;CAEA,OAAO,GAAwB,GAAwD;EACrF,OAAO,EAAK,KAAK,MAAM,OAAO,WAAW;GAAE,OAAO,EAAE,aAAa,EAAI;GAAG;EAAO,CAAC;CAClF;CACA,gBAAwC;EACtC,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,QAAQ,iBAAiB,CAAC;CAC9D;CAEA,MAAc,MAAS,GAA2B;EAChD,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAM,MAAM;EAElB,OADA,EAAK,EAAE,MAAM,mBAAmB,CAAC,GAC1B;CACT;AACF"}
1
+ {"version":3,"file":"taxonomy.js","names":[],"sources":["../../src/client/taxonomy.ts"],"sourcesContent":["import type {\n Assignment,\n AssignmentsWritten,\n ContentRef,\n Count,\n CountsRebuilt,\n Edge,\n EdgesWritten,\n EffectiveTagsOf,\n MergeReport,\n Name,\n Node,\n NodeDetail,\n NodeInput,\n NodePage,\n NodeUpdate,\n} from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, type RouteQuery } from \"./route.js\";\n\n/** GET /taxonomy/nodes filters, as the route names them. */\nexport type NodeQuery = RouteQuery<\"GET\", \"/nodes\">;\n\n/** The taxonomy admin API (root:taxonomy:write). */\nexport class TaxonomyClient {\n constructor(private readonly http: Http) {}\n\n nodes(q: NodeQuery = {}, signal?: AbortSignal): Promise<NodePage> {\n return call(this.http, \"GET\", \"/nodes\", { query: q, signal });\n }\n node(id: string, signal?: AbortSignal): Promise<NodeDetail> {\n return call(this.http, \"GET\", \"/nodes/{id}\", { params: { id }, signal });\n }\n createNodes(nodes: NodeInput[]): Promise<Node[]> {\n return this.write(call(this.http, \"POST\", \"/nodes\", { body: nodes }));\n }\n updateNode(id: string, patch: NodeUpdate): Promise<Node> {\n return this.write(call(this.http, \"PATCH\", \"/nodes/{id}\", { params: { id }, body: patch }));\n }\n /** Marks the node deleted. */\n deleteNode(id: string): Promise<Node> {\n return this.write(call(this.http, \"DELETE\", \"/nodes/{id}\", { params: { id } }));\n }\n /** Replaces, adds or removes names. */\n names(id: string, op: \"set\" | \"add\" | \"remove\", names: Name[]): Promise<NodeDetail> {\n const method = op === \"set\" ? \"PUT\" : op === \"add\" ? \"POST\" : \"DELETE\";\n return this.write(call(this.http, method, \"/nodes/{id}/names\", { params: { id }, body: names }));\n }\n merge(id: string, into: string): Promise<MergeReport> {\n return this.write(call(this.http, \"POST\", \"/nodes/{id}/merge\", { params: { id }, body: { into_taxonomy_id: into } }));\n }\n edges(op: \"add\" | \"remove\", edges: Edge[]): Promise<EdgesWritten> {\n return this.write(call(this.http, op === \"add\" ? \"POST\" : \"DELETE\", \"/edges\", { body: edges }));\n }\n /** Assigns or unassigns nodes; suppressCounts skips count upkeep (rebuildCounts afterwards). */\n assignments(op: \"add\" | \"remove\", assignments: Assignment[], o: { suppressCounts?: boolean } = {}): Promise<AssignmentsWritten> {\n return this.write(call(this.http, op === \"add\" ? \"POST\" : \"DELETE\", \"/assignments\", { body: assignments, query: { suppress_counts: o.suppressCounts } }));\n }\n /** The effective tags (work and version) of each content. */\n effective(refs: ContentRef[], signal?: AbortSignal): Promise<EffectiveTagsOf[]> {\n return call(this.http, \"POST\", \"/effective\", { body: refs, signal });\n }\n /** Content counts of nodes, by taxonomy id. */\n counts(ids: readonly string[], signal?: AbortSignal): Promise<Record<string, Count[]>> {\n return call(this.http, \"GET\", \"/counts\", { query: { taxonomy_id: ids }, signal });\n }\n rebuildCounts(): Promise<CountsRebuilt> {\n return this.write(call(this.http, \"POST\", \"/counts/rebuild\"));\n }\n\n private async write<T>(p: Promise<T>): Promise<T> {\n const emit = this.http.captureChanges();\n const out = await p;\n emit({ type: \"taxonomy.changed\" });\n return out;\n }\n}\n"],"mappings":";;AAwBA,IAAa,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAE1C,MAAM,IAAe,CAAC,GAAG,GAAyC;EAChE,OAAO,EAAK,KAAK,MAAM,OAAO,UAAU;GAAE,OAAO;GAAG;EAAO,CAAC;CAC9D;CACA,KAAK,GAAY,GAA2C;EAC1D,OAAO,EAAK,KAAK,MAAM,OAAO,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG;EAAO,CAAC;CACzE;CACA,YAAY,GAAqC;EAC/C,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,QAAQ,UAAU,EAAE,MAAM,EAAM,CAAC,CAAC;CACtE;CACA,WAAW,GAAY,GAAkC;EACvD,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,SAAS,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM;EAAM,CAAC,CAAC;CAC5F;CAEA,WAAW,GAA2B;EACpC,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,UAAU,eAAe,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC,CAAC;CAChF;CAEA,MAAM,GAAY,GAA8B,GAAoC;EAClF,IAAM,IAAS,MAAO,QAAQ,QAAQ,MAAO,QAAQ,SAAS;EAC9D,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,GAAQ,qBAAqB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM;EAAM,CAAC,CAAC;CACjG;CACA,MAAM,GAAY,GAAoC;EACpD,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,QAAQ,qBAAqB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM,EAAE,kBAAkB,EAAK;EAAE,CAAC,CAAC;CACtH;CACA,MAAM,GAAsB,GAAsC;EAChE,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,MAAO,QAAQ,SAAS,UAAU,UAAU,EAAE,MAAM,EAAM,CAAC,CAAC;CAChG;CAEA,YAAY,GAAsB,GAA2B,IAAkC,CAAC,GAAgC;EAC9H,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,MAAO,QAAQ,SAAS,UAAU,gBAAgB;GAAE,MAAM;GAAa,OAAO,EAAE,iBAAiB,EAAE,eAAe;EAAE,CAAC,CAAC;CAC1J;CAEA,UAAU,GAAoB,GAAkD;EAC9E,OAAO,EAAK,KAAK,MAAM,QAAQ,cAAc;GAAE,MAAM;GAAM;EAAO,CAAC;CACrE;CAEA,OAAO,GAAwB,GAAwD;EACrF,OAAO,EAAK,KAAK,MAAM,OAAO,WAAW;GAAE,OAAO,EAAE,aAAa,EAAI;GAAG;EAAO,CAAC;CAClF;CACA,gBAAwC;EACtC,OAAO,KAAK,MAAM,EAAK,KAAK,MAAM,QAAQ,iBAAiB,CAAC;CAC9D;CAEA,MAAc,MAAS,GAA2B;EAChD,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAM,MAAM;EAElB,OADA,EAAK,EAAE,MAAM,mBAAmB,CAAC,GAC1B;CACT;AACF"}
package/dist/client.js CHANGED
@@ -26,8 +26,8 @@ import { CommentsClient as Re } from "./client/content/comments.js";
26
26
  import { FavoritesClient as ze } from "./client/content/favorites.js";
27
27
  import { ModerationClient as Be } from "./client/content/moderation.js";
28
28
  import { PollsClient as Ve } from "./client/content/polls.js";
29
- import { PostsClient as He } from "./client/content/posts.js";
30
- import { ReactionsClient as Ue } from "./client/content/reactions.js";
31
- import { TaxonomyClient as We } from "./client/taxonomy.js";
32
- import { createContentKitClient as Ge } from "./client/client.js";
33
- export { le as ABR_DEFAULTS, Ie as BansClient, i as CLIENT_ERROR_CODES, Fe as CodesClient, Re as CommentsClient, a as ContentKitError, e as DEFAULT_DENSITY, o as ERROR_CODES, ze as FavoritesClient, S as MAX_PART_SIZE, C as MAX_SINGLE_PUT, w as MIN_PART_SIZE, x as MediaApi, A as MediaClient, Be as ModerationClient, ue as NETWORK_FAILURES_BEFORE_ERROR, ke as PLAYBACK_SPEEDS, Ae as PLAYER_TRACKS_KEY, je as PLAYER_VOLUME_KEY, E as Pacer, Ve as PollsClient, He as PostsClient, ie as READ_CHUNK, de as REFRESH_BEFORE_EXPIRY, Ue as ReactionsClient, We as TaxonomyClient, te as UploadQueue, fe as abrHlsConfig, P as acceptOf, h as aspectOf, j as backoff, _ as call, pe as capRung, G as centeredCrop, y as checkRef, ae as chunkOffsets, me as classifyHlsError, he as classifyMediaError, K as constrainCrop, Ge as createContentKitClient, ne as decodeImage, F as defaultImage, D as defaultTransport, t as densityFor, q as editOf, J as editedSize, ee as encodeRemaining, ge as expiryDelay, s as failureError, O as fetchTransport, I as filesFor, d as fill, Se as formatDuration, Y as fromRotated, Ce as galleryItems, _e as hlsConfig, ve as initialEstimate, re as isAnimatedImage, we as isAudioType, b as isContentId, c as isContentKitError, Te as isImageType, L as isPattern, oe as isProcessing, Ee as isSubtitleType, De as isVideoType, se as mergeReads, Me as parseSpriteVtt, n as pickRendition, Ne as playerKeyAction, ce as processing, f as publicRenditions, p as publicURL, g as ratio, R as ratioLabel, Le as reactionVerb, l as readContentKitError, ye as refreshable, Pe as resumeAt, X as rotation, v as routeURL, z as ruleDir, B as ruleFor, Z as sameEdit, M as samePath, V as screenFiles, T as sha256Hex, r as sortRenditions, m as srcSet, Oe as stageAspect, be as startRung, xe as statusKind, N as stem, u as toContentKitError, Q as toOriginal, $ as toRotated, H as uniqueName, U as uploadRules, W as withMediaType, k as xhrTransport };
29
+ import { IMAGE_SCHEME as He, PostsClient as Ue, imageRef as We, imageRefs as Ge } from "./client/content/posts.js";
30
+ import { ReactionsClient as Ke } from "./client/content/reactions.js";
31
+ import { TaxonomyClient as qe } from "./client/taxonomy.js";
32
+ import { createContentKitClient as Je } from "./client/client.js";
33
+ export { le as ABR_DEFAULTS, Ie as BansClient, i as CLIENT_ERROR_CODES, Fe as CodesClient, Re as CommentsClient, a as ContentKitError, e as DEFAULT_DENSITY, o as ERROR_CODES, ze as FavoritesClient, He as IMAGE_SCHEME, S as MAX_PART_SIZE, C as MAX_SINGLE_PUT, w as MIN_PART_SIZE, x as MediaApi, A as MediaClient, Be as ModerationClient, ue as NETWORK_FAILURES_BEFORE_ERROR, ke as PLAYBACK_SPEEDS, Ae as PLAYER_TRACKS_KEY, je as PLAYER_VOLUME_KEY, E as Pacer, Ve as PollsClient, Ue as PostsClient, ie as READ_CHUNK, de as REFRESH_BEFORE_EXPIRY, Ke as ReactionsClient, qe as TaxonomyClient, te as UploadQueue, fe as abrHlsConfig, P as acceptOf, h as aspectOf, j as backoff, _ as call, pe as capRung, G as centeredCrop, y as checkRef, ae as chunkOffsets, me as classifyHlsError, he as classifyMediaError, K as constrainCrop, Je as createContentKitClient, ne as decodeImage, F as defaultImage, D as defaultTransport, t as densityFor, q as editOf, J as editedSize, ee as encodeRemaining, ge as expiryDelay, s as failureError, O as fetchTransport, I as filesFor, d as fill, Se as formatDuration, Y as fromRotated, Ce as galleryItems, _e as hlsConfig, We as imageRef, Ge as imageRefs, ve as initialEstimate, re as isAnimatedImage, we as isAudioType, b as isContentId, c as isContentKitError, Te as isImageType, L as isPattern, oe as isProcessing, Ee as isSubtitleType, De as isVideoType, se as mergeReads, Me as parseSpriteVtt, n as pickRendition, Ne as playerKeyAction, ce as processing, f as publicRenditions, p as publicURL, g as ratio, R as ratioLabel, Le as reactionVerb, l as readContentKitError, ye as refreshable, Pe as resumeAt, X as rotation, v as routeURL, z as ruleDir, B as ruleFor, Z as sameEdit, M as samePath, V as screenFiles, T as sha256Hex, r as sortRenditions, m as srcSet, Oe as stageAspect, be as startRung, xe as statusKind, N as stem, u as toContentKitError, Q as toOriginal, $ as toRotated, H as uniqueName, U as uploadRules, W as withMediaType, k as xhrTransport };
@@ -25,7 +25,7 @@ export interface PollEditorProps {
25
25
  appearance?: ContentKitUiAppearance;
26
26
  }
27
27
  /**
28
- * Staff: creates or edits a poll (PollWrite): question, language, type, when
28
+ * Staff: creates or edits a poll (root:polls:write): question, language, type, when
29
29
  * it goes live and closes, visibility, a question image, and options with
30
30
  * images, reordered with the move buttons. A new poll is written at once with
31
31
  * its images; an existing one saves its fields with Save and each option
@@ -1 +1 @@
1
- {"version":3,"file":"poll-editor.js","names":[],"sources":["../../../src/components/social/poll-editor.tsx"],"sourcesContent":["import { Delete02Icon, ImageAdd01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useEffect, useId, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { Poll, PollKind, PollOption, PollUpdate } from \"../../client/generated/wire.js\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useErrorReporter, type ContentKitErrorHandler } from \"../../react/context.js\";\nimport { usePollEditor } from \"../../react/polls.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { Switch } from \"#ckui/ui/switch\";\nimport { Textarea } from \"#ckui/ui/textarea\";\nimport { localInput } from \"./bans.js\";\nimport { SortableList } from \"../sortable-list.js\";\nimport { ConfirmDialog, contentError, ErrorLine } from \"./parts.js\";\n\nexport interface PollEditorProps {\n /** The poll to edit; none creates one. */\n poll?: string | null;\n /** Languages offered for a new poll: codes, or codes with labels. Without them the language is typed. */\n languages?: readonly (string | { value: string; label: string })[];\n /** A new poll's language. Default the first offered, else \"en\". */\n defaultLanguage?: string;\n /** Offer free-text polls (the host registered an answer classifier). Default false. */\n freeText?: boolean;\n /** Most options. Default 10. */\n maxOptions?: number;\n onCreated?: (poll: Poll) => void;\n onSaved?: (poll: Poll) => void;\n onDeleted?: () => void;\n client?: ContentKitClient;\n onError?: ContentKitErrorHandler;\n className?: string;\n appearance?: ContentKitUiAppearance;\n}\n\ninterface Draft {\n key: string;\n label: string;\n image: File | null;\n}\n\nconst toISO = (local: string) => (local ? new Date(local).toISOString() : undefined);\nlet seq = 0;\nconst draft = (): Draft => ({ key: `o${++seq}`, label: \"\", image: null });\n\n/**\n * Staff: creates or edits a poll (PollWrite): question, language, type, when\n * it goes live and closes, visibility, a question image, and options with\n * images, reordered with the move buttons. A new poll is written at once with\n * its images; an existing one saves its fields with Save and each option\n * change as it is made.\n */\nexport function PollEditor(p: PollEditorProps) {\n const m = useMessages();\n const { t } = m;\n const ed = usePollEditor(p.poll ?? null, { client: p.client });\n const report = useErrorReporter(p.onError);\n const [error, setError] = useState<ContentKitError | string | null>(null);\n const [notice, setNotice] = useState<string | null>(null);\n const [deleting, setDeleting] = useState(false);\n const fail = (e: unknown) => {\n const err = toContentKitError(e);\n setError(err);\n report(err, \"poll.save\");\n };\n const root = (children: ReactNode) => (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-5 text-sm\", p.className)} data-ckui=\"poll-editor\">\n {children}\n </ContentKitUiRoot>\n );\n if (ed.loading && !ed.poll) {\n return root(\n <div className=\"grid gap-3\" aria-busy>\n <Skeleton className=\"h-9 w-full\" />\n <Skeleton className=\"h-24 w-full\" />\n </div>,\n );\n }\n if (ed.error && !ed.poll) return root(<ErrorLine error={ed.error} />);\n return root(\n <>\n <h2 className=\"text-base font-semibold\">{t(ed.poll ? \"pollEditor.editTitle\" : \"pollEditor.newTitle\")}</h2>\n {ed.poll ? (\n <EditFields\n key={ed.poll.id}\n poll={ed.poll}\n saving={ed.saving}\n maxOptions={p.maxOptions ?? 10}\n editor={ed}\n onError={fail}\n onInvalid={(msg) => setError(msg)}\n onSaved={(poll) => {\n setError(null);\n setNotice(t(\"pollEditor.saved\"));\n p.onSaved?.(poll);\n }}\n />\n ) : (\n <CreateFields\n saving={ed.saving}\n languages={p.languages}\n defaultLanguage={p.defaultLanguage}\n freeText={!!p.freeText}\n maxOptions={p.maxOptions ?? 10}\n onInvalid={(msg) => setError(msg)}\n onCreate={async (input, images) => {\n setError(null);\n try {\n const { poll, imageErrors } = await ed.create(input, images);\n setNotice(imageErrors.length ? t(\"pollEditor.imageFailed\") : t(\"pollEditor.created\"));\n for (const e of imageErrors) report(e, \"poll.save\");\n p.onCreated?.(poll);\n } catch (e) {\n fail(e);\n }\n }}\n />\n )}\n {error && <ErrorLine>{typeof error === \"string\" ? error : contentError(m, error)}</ErrorLine>}\n {notice && !error && (\n <p role=\"status\" className=\"text-sm text-muted-foreground\">\n {notice}\n </p>\n )}\n {ed.poll && (\n <div className=\"border-t border-border pt-4\">\n <Button variant=\"destructive\" onClick={() => setDeleting(true)} disabled={ed.saving}>\n {t(\"pollEditor.delete\")}\n </Button>\n </div>\n )}\n <ConfirmDialog\n open={deleting}\n onOpenChange={setDeleting}\n title={t(\"pollEditor.deleteTitle\")}\n description={t(\"pollEditor.deleteBody\")}\n confirm={t(\"pollEditor.delete\")}\n onConfirm={() =>\n void ed.remove().then(\n () => p.onDeleted?.(),\n (e) => fail(e),\n )\n }\n />\n </>,\n );\n}\n\nfunction CreateFields(p: {\n saving: boolean;\n languages?: PollEditorProps[\"languages\"];\n defaultLanguage?: string;\n freeText: boolean;\n maxOptions: number;\n onInvalid: (message: string) => void;\n onCreate: (input: Parameters<ReturnType<typeof usePollEditor>[\"create\"]>[0], images: { question: File | null; options: (File | null)[] }) => Promise<void>;\n}) {\n const { t } = useMessages();\n const ids = useId();\n const langs = useMemo(() => (p.languages ?? []).map((l) => (typeof l === \"string\" ? { value: l, label: l } : l)), [p.languages]);\n const [question, setQuestion] = useState(\"\");\n const [language, setLanguage] = useState(p.defaultLanguage ?? langs[0]?.value ?? \"en\");\n const [kind, setKind] = useState<PollKind>(\"multiple_choice\");\n const [liveAt, setLiveAt] = useState(() => localInput(new Date()));\n const [closesAt, setClosesAt] = useState(\"\");\n const [image, setImage] = useState<File | null>(null);\n const [options, setOptions] = useState<Draft[]>(() => [draft(), draft()]);\n const set = (key: string, patch: Partial<Draft>) => setOptions((os) => os.map((o) => (o.key === key ? { ...o, ...patch } : o)));\n const move = (from: number, to: number) =>\n setOptions((os) => {\n const next = [...os];\n next.splice(to, 0, ...next.splice(from, 1));\n return next;\n });\n const submit = () => {\n if (!question.trim()) return p.onInvalid(t(\"pollEditor.questionRequired\"));\n const kept = options.filter((o) => o.label.trim() || o.image);\n if (kind === \"multiple_choice\") {\n if (kept.some((o) => !o.label.trim())) return p.onInvalid(t(\"pollEditor.optionRequired\"));\n if (kept.length < 2) return p.onInvalid(t(\"pollEditor.minOptions\"));\n }\n const choice = kind === \"multiple_choice\";\n void p.onCreate(\n {\n kind,\n question: question.trim(),\n language,\n live_at: toISO(liveAt),\n closes_at: toISO(closesAt),\n options: choice ? kept.map((o, position) => ({ label: o.label.trim(), position })) : undefined,\n },\n { question: image, options: choice ? kept.map((o) => o.image) : [] },\n );\n };\n return (\n <form\n className=\"grid gap-4\"\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n <Field id={`${ids}-q`} label={t(\"pollEditor.question\")}>\n <Textarea id={`${ids}-q`} rows={2} value={question} placeholder={t(\"pollEditor.questionPlaceholder\")} onChange={(e) => setQuestion(e.target.value)} />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field id={`${ids}-lang`} label={t(\"pollEditor.language\")}>\n {langs.length ? (\n <Select value={language} onValueChange={(v) => setLanguage(String(v))}>\n <SelectTrigger id={`${ids}-lang`} className=\"w-full\">\n <SelectValue>{(v: string) => langs.find((l) => l.value === v)?.label ?? v}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {langs.map((l) => (\n <SelectItem key={l.value} value={l.value}>\n {l.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <Input id={`${ids}-lang`} value={language} onChange={(e) => setLanguage(e.target.value)} />\n )}\n </Field>\n {p.freeText && (\n <fieldset className=\"grid gap-1.5\">\n <legend className=\"mb-1.5 text-sm font-medium\">{t(\"pollEditor.kind\")}</legend>\n <div className=\"flex flex-wrap gap-2\">\n {([\"multiple_choice\", \"free_text\"] as const).map((k) => (\n <Button key={k} type=\"button\" size=\"sm\" variant={kind === k ? \"default\" : \"outline\"} aria-pressed={kind === k} onClick={() => setKind(k)}>\n {t(k === \"free_text\" ? \"pollEditor.freeText\" : \"pollEditor.multipleChoice\")}\n </Button>\n ))}\n </div>\n </fieldset>\n )}\n <Field id={`${ids}-live`} label={t(\"pollEditor.liveAt\")}>\n <Input id={`${ids}-live`} type=\"datetime-local\" value={liveAt} onChange={(e) => setLiveAt(e.target.value)} />\n </Field>\n <Field id={`${ids}-closes`} label={t(\"pollEditor.closesAt\")}>\n <Input id={`${ids}-closes`} type=\"datetime-local\" value={closesAt} min={liveAt} onChange={(e) => setClosesAt(e.target.value)} />\n </Field>\n </div>\n <ImageField label={t(\"pollEditor.image\")} url={useObjectURL(image)} onPick={setImage} onRemove={() => setImage(null)} />\n {kind === \"free_text\" ? (\n <p className=\"text-muted-foreground\">{t(\"pollEditor.freeTextHelp\")}</p>\n ) : (\n <fieldset className=\"grid gap-2\">\n <legend className=\"mb-1.5 text-sm font-medium\">{t(\"pollEditor.options\")}</legend>\n <SortableList\n items={options}\n id={(o) => o.key}\n name={(o) => o.label || t(\"pollEditor.option\", { index: options.indexOf(o) + 1 })}\n onMove={move}\n label={t(\"pollEditor.options\")}\n className=\"gap-2\"\n row={() => ({ className: OPTION_ROW, \"data-ckui\": \"poll-option\" })}\n >\n {(o, handle, i) => (\n <OptionRow\n handle={handle}\n index={i}\n label={o.label}\n image={o.image}\n onLabel={(label) => set(o.key, { label })}\n onImage={(image) => set(o.key, { image })}\n onRemove={options.length > 2 ? () => setOptions((os) => os.filter((x) => x.key !== o.key)) : undefined}\n />\n )}\n </SortableList>\n {options.length < p.maxOptions ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"justify-self-start\" onClick={() => setOptions((os) => [...os, draft()])}>\n {t(\"pollEditor.addOption\")}\n </Button>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{t(\"pollEditor.maxOptions\", { max: p.maxOptions })}</p>\n )}\n </fieldset>\n )}\n <Button type=\"submit\" className=\"justify-self-start\" disabled={p.saving}>\n {p.saving ? t(\"pollEditor.creating\") : t(\"pollEditor.create\")}\n </Button>\n </form>\n );\n}\n\nfunction EditFields(p: {\n poll: Poll;\n saving: boolean;\n maxOptions: number;\n editor: ReturnType<typeof usePollEditor>;\n onError: (e: unknown) => void;\n onInvalid: (message: string) => void;\n onSaved: (poll: Poll) => void;\n}) {\n const m = useMessages();\n const { t } = m;\n const ids = useId();\n const { poll, editor } = p;\n const [question, setQuestion] = useState(poll.question);\n const [liveAt, setLiveAt] = useState(localInput(new Date(poll.live_at)));\n const [closesAt, setClosesAt] = useState(poll.closes_at ? localInput(new Date(poll.closes_at)) : \"\");\n const [active, setActive] = useState(poll.is_active);\n const [added, setAdded] = useState(\"\");\n const [removing, setRemoving] = useState<PollOption | null>(null);\n const options = [...poll.options].sort((a, b) => a.position - b.position);\n const patch: PollUpdate = {};\n if (question.trim() !== poll.question) patch.question = question.trim();\n if (toISO(liveAt) !== new Date(localInput(new Date(poll.live_at))).toISOString()) patch.live_at = toISO(liveAt);\n if (closesAt && toISO(closesAt) !== (poll.closes_at && new Date(localInput(new Date(poll.closes_at))).toISOString())) patch.closes_at = toISO(closesAt);\n else if (!closesAt && poll.closes_at) patch.closes_at = null;\n if (active !== poll.is_active) patch.is_active = active;\n const dirty = Object.keys(patch).length > 0;\n const act = (fn: () => Promise<unknown>) => void fn().catch(p.onError);\n return (\n <div className=\"grid gap-4\">\n <form\n className=\"grid gap-4\"\n onSubmit={(e) => {\n e.preventDefault();\n if (!question.trim()) return p.onInvalid(t(\"pollEditor.questionRequired\"));\n if (dirty) act(async () => p.onSaved(await editor.update(patch)));\n }}\n >\n <Field id={`${ids}-q`} label={t(\"pollEditor.question\")}>\n <Textarea id={`${ids}-q`} rows={2} value={question} onChange={(e) => setQuestion(e.target.value)} />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field id={`${ids}-live`} label={t(\"pollEditor.liveAt\")}>\n <Input id={`${ids}-live`} type=\"datetime-local\" value={liveAt} onChange={(e) => setLiveAt(e.target.value)} />\n </Field>\n <Field id={`${ids}-closes`} label={t(\"pollEditor.closesAt\")}>\n <Input id={`${ids}-closes`} type=\"datetime-local\" value={closesAt} min={liveAt} onChange={(e) => setClosesAt(e.target.value)} />\n </Field>\n </div>\n <div className=\"flex items-center gap-3\">\n <Switch id={`${ids}-active`} checked={active} onCheckedChange={setActive} />\n <Label htmlFor={`${ids}-active`}>{t(active ? \"pollEditor.active\" : \"pollEditor.inactive\")}</Label>\n </div>\n <Button type=\"submit\" className=\"justify-self-start\" disabled={!dirty || p.saving}>\n {p.saving ? t(\"common.saving\") : t(\"common.save\")}\n </Button>\n </form>\n <ImageField label={t(\"pollEditor.image\")} url={poll.image_url} disabled={p.saving} onPick={(f) => act(() => editor.setImage(f))} onRemove={() => act(() => editor.setImage(null))} />\n {poll.kind === \"free_text\" ? (\n <p className=\"text-muted-foreground\">\n {t(\"pollEditor.freeTextHelp\")} {m.plural(\"poll.answers\", poll.answer_count ?? 0)}\n </p>\n ) : (\n <fieldset className=\"grid gap-2\">\n <legend className=\"mb-1.5 text-sm font-medium\">{t(\"pollEditor.options\")}</legend>\n <SortableList\n items={options}\n id={(o) => o.id}\n name={(o) => o.label}\n disabled={p.saving}\n onMove={(from, to) => act(() => editor.moveOption(options[from]!.id, to))}\n label={t(\"pollEditor.options\")}\n className=\"gap-2\"\n row={() => ({ className: OPTION_ROW, \"data-ckui\": \"poll-option\" })}\n >\n {(o, handle, i) => (\n <OptionRow\n handle={handle}\n index={i}\n label={o.label}\n url={o.image_url}\n votes={o.vote_count}\n disabled={p.saving}\n onCommit={(label) => label.trim() && label.trim() !== o.label && act(() => editor.renameOption(o.id, label.trim()))}\n onImage={(f) => act(() => editor.setOptionImage(o.id, f))}\n onRemove={options.length > 2 ? () => setRemoving(o) : undefined}\n />\n )}\n </SortableList>\n {options.length < p.maxOptions ? (\n <form\n className=\"flex gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n if (!added.trim()) return;\n act(async () => {\n await editor.addOption(added.trim());\n setAdded(\"\");\n });\n }}\n >\n <Input value={added} placeholder={t(\"pollEditor.newOption\")} aria-label={t(\"pollEditor.newOption\")} onChange={(e) => setAdded(e.target.value)} />\n <Button type=\"submit\" variant=\"outline\" disabled={!added.trim() || p.saving}>\n {t(\"pollEditor.addOption\")}\n </Button>\n </form>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{t(\"pollEditor.maxOptions\", { max: p.maxOptions })}</p>\n )}\n </fieldset>\n )}\n <ConfirmDialog\n open={!!removing}\n onOpenChange={(o) => !o && setRemoving(null)}\n title={t(\"pollEditor.removeOptionTitle\")}\n description={t(\"pollEditor.removeOptionBody\")}\n confirm={t(\"pollEditor.removeOption\")}\n onConfirm={() => removing && act(() => editor.removeOption(removing.id))}\n />\n </div>\n );\n}\n\nconst OPTION_ROW = \"flex flex-wrap items-center gap-2 rounded-lg border border-border p-2\";\n\n/** One option's content: its drag handle, label, image, votes and remove. */\nfunction OptionRow(p: {\n handle: ReactNode;\n index: number;\n label: string;\n /** A new poll's staged image. */\n image?: File | null;\n /** An existing option's image. */\n url?: string;\n votes?: number;\n disabled?: boolean;\n /** A new poll's label as typed. */\n onLabel?: (label: string) => void;\n /** An existing option's label, on blur or Enter. */\n onCommit?: (label: string) => void;\n onImage: (file: File | null) => void;\n onRemove?: () => void;\n}) {\n const m = useMessages();\n const { t } = m;\n const [label, setLabel] = useState(p.label);\n useEffect(() => setLabel(p.label), [p.label]);\n const staged = useObjectURL(p.image ?? null);\n const name = t(\"pollEditor.option\", { index: p.index + 1 });\n return (\n <>\n {p.handle}\n <Input\n className=\"min-w-40 flex-1\"\n value={label}\n aria-label={name}\n placeholder={name}\n disabled={p.disabled && !p.onLabel}\n onChange={(e) => {\n setLabel(e.target.value);\n p.onLabel?.(e.target.value);\n }}\n onBlur={() => p.onCommit?.(label)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && p.onCommit) {\n e.preventDefault();\n p.onCommit(label);\n }\n }}\n />\n <ImageField compact label={t(\"pollEditor.optionImage\", { index: p.index + 1 })} url={p.url ?? staged} disabled={p.disabled} onPick={p.onImage} onRemove={() => p.onImage(null)} />\n {p.votes !== undefined && <span className=\"text-xs tabular-nums text-muted-foreground\">{m.plural(\"poll.votes\", p.votes)}</span>}\n {p.onRemove && (\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={`${t(\"pollEditor.removeOption\")}: ${name}`} disabled={p.disabled} onClick={p.onRemove}>\n <HugeiconsIcon icon={Delete02Icon} strokeWidth={2} />\n </Button>\n )}\n </>\n );\n}\n\nfunction ImageField(p: { label: string; url?: string | null; compact?: boolean; disabled?: boolean; onPick: (file: File) => void; onRemove: () => void }) {\n const { t } = useMessages();\n const input = useRef<HTMLInputElement>(null);\n const pick = (\n <input\n ref={input}\n type=\"file\"\n accept=\"image/*\"\n hidden\n aria-label={p.label}\n onChange={(e) => {\n const f = e.target.files?.[0];\n e.target.value = \"\";\n if (f) p.onPick(f);\n }}\n />\n );\n if (p.compact) {\n return (\n <div className=\"flex items-center gap-1\" data-ckui=\"poll-image\">\n {pick}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n className=\"overflow-hidden p-0\"\n aria-label={`${p.url ? t(\"pollEditor.replaceImage\") : t(\"pollEditor.uploadImage\")}: ${p.label}`}\n disabled={p.disabled}\n onClick={() => input.current?.click()}\n >\n {p.url ? <img src={p.url} alt=\"\" className=\"size-full object-cover\" /> : <HugeiconsIcon icon={ImageAdd01Icon} strokeWidth={2} />}\n </Button>\n {p.url && (\n <Button type=\"button\" variant=\"ghost\" size=\"xs\" disabled={p.disabled} onClick={p.onRemove}>\n {t(\"pollEditor.removeImage\")}\n </Button>\n )}\n </div>\n );\n }\n return (\n <div className=\"grid gap-1.5\" data-ckui=\"poll-image\">\n <span className=\"text-sm font-medium\">{p.label}</span>\n {pick}\n {p.url && <img src={p.url} alt=\"\" className=\"aspect-video w-full max-w-sm rounded-lg border border-border object-cover\" />}\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={p.disabled} onClick={() => input.current?.click()}>\n <HugeiconsIcon icon={ImageAdd01Icon} strokeWidth={2} data-icon=\"inline-start\" />\n {p.url ? t(\"pollEditor.replaceImage\") : t(\"pollEditor.uploadImage\")}\n </Button>\n {p.url && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" disabled={p.disabled} onClick={p.onRemove}>\n {t(\"pollEditor.removeImage\")}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\nfunction Field({ id, label, children }: { id: string; label: string; children: ReactNode }) {\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n {children}\n </div>\n );\n}\n\n/** An object URL for a staged file, revoked when it changes. */\nfunction useObjectURL(file: File | null): string | undefined {\n const [url, setUrl] = useState<string>();\n useEffect(() => {\n if (!file) return setUrl(undefined);\n const u = URL.createObjectURL(file);\n setUrl(u);\n return () => URL.revokeObjectURL(u);\n }, [file]);\n return url;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"poll-editor.js","names":[],"sources":["../../../src/components/social/poll-editor.tsx"],"sourcesContent":["import { Delete02Icon, ImageAdd01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useEffect, useId, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../../client/errors.js\";\nimport type { Poll, PollKind, PollOption, PollUpdate } from \"../../client/generated/wire.js\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useErrorReporter, type ContentKitErrorHandler } from \"../../react/context.js\";\nimport { usePollEditor } from \"../../react/polls.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { Input } from \"#ckui/ui/input\";\nimport { Label } from \"#ckui/ui/label\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"#ckui/ui/select\";\nimport { Skeleton } from \"#ckui/ui/skeleton\";\nimport { Switch } from \"#ckui/ui/switch\";\nimport { Textarea } from \"#ckui/ui/textarea\";\nimport { localInput } from \"./bans.js\";\nimport { SortableList } from \"../sortable-list.js\";\nimport { ConfirmDialog, contentError, ErrorLine } from \"./parts.js\";\n\nexport interface PollEditorProps {\n /** The poll to edit; none creates one. */\n poll?: string | null;\n /** Languages offered for a new poll: codes, or codes with labels. Without them the language is typed. */\n languages?: readonly (string | { value: string; label: string })[];\n /** A new poll's language. Default the first offered, else \"en\". */\n defaultLanguage?: string;\n /** Offer free-text polls (the host registered an answer classifier). Default false. */\n freeText?: boolean;\n /** Most options. Default 10. */\n maxOptions?: number;\n onCreated?: (poll: Poll) => void;\n onSaved?: (poll: Poll) => void;\n onDeleted?: () => void;\n client?: ContentKitClient;\n onError?: ContentKitErrorHandler;\n className?: string;\n appearance?: ContentKitUiAppearance;\n}\n\ninterface Draft {\n key: string;\n label: string;\n image: File | null;\n}\n\nconst toISO = (local: string) => (local ? new Date(local).toISOString() : undefined);\nlet seq = 0;\nconst draft = (): Draft => ({ key: `o${++seq}`, label: \"\", image: null });\n\n/**\n * Staff: creates or edits a poll (root:polls:write): question, language, type, when\n * it goes live and closes, visibility, a question image, and options with\n * images, reordered with the move buttons. A new poll is written at once with\n * its images; an existing one saves its fields with Save and each option\n * change as it is made.\n */\nexport function PollEditor(p: PollEditorProps) {\n const m = useMessages();\n const { t } = m;\n const ed = usePollEditor(p.poll ?? null, { client: p.client });\n const report = useErrorReporter(p.onError);\n const [error, setError] = useState<ContentKitError | string | null>(null);\n const [notice, setNotice] = useState<string | null>(null);\n const [deleting, setDeleting] = useState(false);\n const fail = (e: unknown) => {\n const err = toContentKitError(e);\n setError(err);\n report(err, \"poll.save\");\n };\n const root = (children: ReactNode) => (\n <ContentKitUiRoot appearance={p.appearance} className={cn(\"grid gap-5 text-sm\", p.className)} data-ckui=\"poll-editor\">\n {children}\n </ContentKitUiRoot>\n );\n if (ed.loading && !ed.poll) {\n return root(\n <div className=\"grid gap-3\" aria-busy>\n <Skeleton className=\"h-9 w-full\" />\n <Skeleton className=\"h-24 w-full\" />\n </div>,\n );\n }\n if (ed.error && !ed.poll) return root(<ErrorLine error={ed.error} />);\n return root(\n <>\n <h2 className=\"text-base font-semibold\">{t(ed.poll ? \"pollEditor.editTitle\" : \"pollEditor.newTitle\")}</h2>\n {ed.poll ? (\n <EditFields\n key={ed.poll.id}\n poll={ed.poll}\n saving={ed.saving}\n maxOptions={p.maxOptions ?? 10}\n editor={ed}\n onError={fail}\n onInvalid={(msg) => setError(msg)}\n onSaved={(poll) => {\n setError(null);\n setNotice(t(\"pollEditor.saved\"));\n p.onSaved?.(poll);\n }}\n />\n ) : (\n <CreateFields\n saving={ed.saving}\n languages={p.languages}\n defaultLanguage={p.defaultLanguage}\n freeText={!!p.freeText}\n maxOptions={p.maxOptions ?? 10}\n onInvalid={(msg) => setError(msg)}\n onCreate={async (input, images) => {\n setError(null);\n try {\n const { poll, imageErrors } = await ed.create(input, images);\n setNotice(imageErrors.length ? t(\"pollEditor.imageFailed\") : t(\"pollEditor.created\"));\n for (const e of imageErrors) report(e, \"poll.save\");\n p.onCreated?.(poll);\n } catch (e) {\n fail(e);\n }\n }}\n />\n )}\n {error && <ErrorLine>{typeof error === \"string\" ? error : contentError(m, error)}</ErrorLine>}\n {notice && !error && (\n <p role=\"status\" className=\"text-sm text-muted-foreground\">\n {notice}\n </p>\n )}\n {ed.poll && (\n <div className=\"border-t border-border pt-4\">\n <Button variant=\"destructive\" onClick={() => setDeleting(true)} disabled={ed.saving}>\n {t(\"pollEditor.delete\")}\n </Button>\n </div>\n )}\n <ConfirmDialog\n open={deleting}\n onOpenChange={setDeleting}\n title={t(\"pollEditor.deleteTitle\")}\n description={t(\"pollEditor.deleteBody\")}\n confirm={t(\"pollEditor.delete\")}\n onConfirm={() =>\n void ed.remove().then(\n () => p.onDeleted?.(),\n (e) => fail(e),\n )\n }\n />\n </>,\n );\n}\n\nfunction CreateFields(p: {\n saving: boolean;\n languages?: PollEditorProps[\"languages\"];\n defaultLanguage?: string;\n freeText: boolean;\n maxOptions: number;\n onInvalid: (message: string) => void;\n onCreate: (input: Parameters<ReturnType<typeof usePollEditor>[\"create\"]>[0], images: { question: File | null; options: (File | null)[] }) => Promise<void>;\n}) {\n const { t } = useMessages();\n const ids = useId();\n const langs = useMemo(() => (p.languages ?? []).map((l) => (typeof l === \"string\" ? { value: l, label: l } : l)), [p.languages]);\n const [question, setQuestion] = useState(\"\");\n const [language, setLanguage] = useState(p.defaultLanguage ?? langs[0]?.value ?? \"en\");\n const [kind, setKind] = useState<PollKind>(\"multiple_choice\");\n const [liveAt, setLiveAt] = useState(() => localInput(new Date()));\n const [closesAt, setClosesAt] = useState(\"\");\n const [image, setImage] = useState<File | null>(null);\n const [options, setOptions] = useState<Draft[]>(() => [draft(), draft()]);\n const set = (key: string, patch: Partial<Draft>) => setOptions((os) => os.map((o) => (o.key === key ? { ...o, ...patch } : o)));\n const move = (from: number, to: number) =>\n setOptions((os) => {\n const next = [...os];\n next.splice(to, 0, ...next.splice(from, 1));\n return next;\n });\n const submit = () => {\n if (!question.trim()) return p.onInvalid(t(\"pollEditor.questionRequired\"));\n const kept = options.filter((o) => o.label.trim() || o.image);\n if (kind === \"multiple_choice\") {\n if (kept.some((o) => !o.label.trim())) return p.onInvalid(t(\"pollEditor.optionRequired\"));\n if (kept.length < 2) return p.onInvalid(t(\"pollEditor.minOptions\"));\n }\n const choice = kind === \"multiple_choice\";\n void p.onCreate(\n {\n kind,\n question: question.trim(),\n language,\n live_at: toISO(liveAt),\n closes_at: toISO(closesAt),\n options: choice ? kept.map((o, position) => ({ label: o.label.trim(), position })) : undefined,\n },\n { question: image, options: choice ? kept.map((o) => o.image) : [] },\n );\n };\n return (\n <form\n className=\"grid gap-4\"\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n <Field id={`${ids}-q`} label={t(\"pollEditor.question\")}>\n <Textarea id={`${ids}-q`} rows={2} value={question} placeholder={t(\"pollEditor.questionPlaceholder\")} onChange={(e) => setQuestion(e.target.value)} />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field id={`${ids}-lang`} label={t(\"pollEditor.language\")}>\n {langs.length ? (\n <Select value={language} onValueChange={(v) => setLanguage(String(v))}>\n <SelectTrigger id={`${ids}-lang`} className=\"w-full\">\n <SelectValue>{(v: string) => langs.find((l) => l.value === v)?.label ?? v}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {langs.map((l) => (\n <SelectItem key={l.value} value={l.value}>\n {l.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <Input id={`${ids}-lang`} value={language} onChange={(e) => setLanguage(e.target.value)} />\n )}\n </Field>\n {p.freeText && (\n <fieldset className=\"grid gap-1.5\">\n <legend className=\"mb-1.5 text-sm font-medium\">{t(\"pollEditor.kind\")}</legend>\n <div className=\"flex flex-wrap gap-2\">\n {([\"multiple_choice\", \"free_text\"] as const).map((k) => (\n <Button key={k} type=\"button\" size=\"sm\" variant={kind === k ? \"default\" : \"outline\"} aria-pressed={kind === k} onClick={() => setKind(k)}>\n {t(k === \"free_text\" ? \"pollEditor.freeText\" : \"pollEditor.multipleChoice\")}\n </Button>\n ))}\n </div>\n </fieldset>\n )}\n <Field id={`${ids}-live`} label={t(\"pollEditor.liveAt\")}>\n <Input id={`${ids}-live`} type=\"datetime-local\" value={liveAt} onChange={(e) => setLiveAt(e.target.value)} />\n </Field>\n <Field id={`${ids}-closes`} label={t(\"pollEditor.closesAt\")}>\n <Input id={`${ids}-closes`} type=\"datetime-local\" value={closesAt} min={liveAt} onChange={(e) => setClosesAt(e.target.value)} />\n </Field>\n </div>\n <ImageField label={t(\"pollEditor.image\")} url={useObjectURL(image)} onPick={setImage} onRemove={() => setImage(null)} />\n {kind === \"free_text\" ? (\n <p className=\"text-muted-foreground\">{t(\"pollEditor.freeTextHelp\")}</p>\n ) : (\n <fieldset className=\"grid gap-2\">\n <legend className=\"mb-1.5 text-sm font-medium\">{t(\"pollEditor.options\")}</legend>\n <SortableList\n items={options}\n id={(o) => o.key}\n name={(o) => o.label || t(\"pollEditor.option\", { index: options.indexOf(o) + 1 })}\n onMove={move}\n label={t(\"pollEditor.options\")}\n className=\"gap-2\"\n row={() => ({ className: OPTION_ROW, \"data-ckui\": \"poll-option\" })}\n >\n {(o, handle, i) => (\n <OptionRow\n handle={handle}\n index={i}\n label={o.label}\n image={o.image}\n onLabel={(label) => set(o.key, { label })}\n onImage={(image) => set(o.key, { image })}\n onRemove={options.length > 2 ? () => setOptions((os) => os.filter((x) => x.key !== o.key)) : undefined}\n />\n )}\n </SortableList>\n {options.length < p.maxOptions ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"justify-self-start\" onClick={() => setOptions((os) => [...os, draft()])}>\n {t(\"pollEditor.addOption\")}\n </Button>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{t(\"pollEditor.maxOptions\", { max: p.maxOptions })}</p>\n )}\n </fieldset>\n )}\n <Button type=\"submit\" className=\"justify-self-start\" disabled={p.saving}>\n {p.saving ? t(\"pollEditor.creating\") : t(\"pollEditor.create\")}\n </Button>\n </form>\n );\n}\n\nfunction EditFields(p: {\n poll: Poll;\n saving: boolean;\n maxOptions: number;\n editor: ReturnType<typeof usePollEditor>;\n onError: (e: unknown) => void;\n onInvalid: (message: string) => void;\n onSaved: (poll: Poll) => void;\n}) {\n const m = useMessages();\n const { t } = m;\n const ids = useId();\n const { poll, editor } = p;\n const [question, setQuestion] = useState(poll.question);\n const [liveAt, setLiveAt] = useState(localInput(new Date(poll.live_at)));\n const [closesAt, setClosesAt] = useState(poll.closes_at ? localInput(new Date(poll.closes_at)) : \"\");\n const [active, setActive] = useState(poll.is_active);\n const [added, setAdded] = useState(\"\");\n const [removing, setRemoving] = useState<PollOption | null>(null);\n const options = [...poll.options].sort((a, b) => a.position - b.position);\n const patch: PollUpdate = {};\n if (question.trim() !== poll.question) patch.question = question.trim();\n if (toISO(liveAt) !== new Date(localInput(new Date(poll.live_at))).toISOString()) patch.live_at = toISO(liveAt);\n if (closesAt && toISO(closesAt) !== (poll.closes_at && new Date(localInput(new Date(poll.closes_at))).toISOString())) patch.closes_at = toISO(closesAt);\n else if (!closesAt && poll.closes_at) patch.closes_at = null;\n if (active !== poll.is_active) patch.is_active = active;\n const dirty = Object.keys(patch).length > 0;\n const act = (fn: () => Promise<unknown>) => void fn().catch(p.onError);\n return (\n <div className=\"grid gap-4\">\n <form\n className=\"grid gap-4\"\n onSubmit={(e) => {\n e.preventDefault();\n if (!question.trim()) return p.onInvalid(t(\"pollEditor.questionRequired\"));\n if (dirty) act(async () => p.onSaved(await editor.update(patch)));\n }}\n >\n <Field id={`${ids}-q`} label={t(\"pollEditor.question\")}>\n <Textarea id={`${ids}-q`} rows={2} value={question} onChange={(e) => setQuestion(e.target.value)} />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field id={`${ids}-live`} label={t(\"pollEditor.liveAt\")}>\n <Input id={`${ids}-live`} type=\"datetime-local\" value={liveAt} onChange={(e) => setLiveAt(e.target.value)} />\n </Field>\n <Field id={`${ids}-closes`} label={t(\"pollEditor.closesAt\")}>\n <Input id={`${ids}-closes`} type=\"datetime-local\" value={closesAt} min={liveAt} onChange={(e) => setClosesAt(e.target.value)} />\n </Field>\n </div>\n <div className=\"flex items-center gap-3\">\n <Switch id={`${ids}-active`} checked={active} onCheckedChange={setActive} />\n <Label htmlFor={`${ids}-active`}>{t(active ? \"pollEditor.active\" : \"pollEditor.inactive\")}</Label>\n </div>\n <Button type=\"submit\" className=\"justify-self-start\" disabled={!dirty || p.saving}>\n {p.saving ? t(\"common.saving\") : t(\"common.save\")}\n </Button>\n </form>\n <ImageField label={t(\"pollEditor.image\")} url={poll.image_url} disabled={p.saving} onPick={(f) => act(() => editor.setImage(f))} onRemove={() => act(() => editor.setImage(null))} />\n {poll.kind === \"free_text\" ? (\n <p className=\"text-muted-foreground\">\n {t(\"pollEditor.freeTextHelp\")} {m.plural(\"poll.answers\", poll.answer_count ?? 0)}\n </p>\n ) : (\n <fieldset className=\"grid gap-2\">\n <legend className=\"mb-1.5 text-sm font-medium\">{t(\"pollEditor.options\")}</legend>\n <SortableList\n items={options}\n id={(o) => o.id}\n name={(o) => o.label}\n disabled={p.saving}\n onMove={(from, to) => act(() => editor.moveOption(options[from]!.id, to))}\n label={t(\"pollEditor.options\")}\n className=\"gap-2\"\n row={() => ({ className: OPTION_ROW, \"data-ckui\": \"poll-option\" })}\n >\n {(o, handle, i) => (\n <OptionRow\n handle={handle}\n index={i}\n label={o.label}\n url={o.image_url}\n votes={o.vote_count}\n disabled={p.saving}\n onCommit={(label) => label.trim() && label.trim() !== o.label && act(() => editor.renameOption(o.id, label.trim()))}\n onImage={(f) => act(() => editor.setOptionImage(o.id, f))}\n onRemove={options.length > 2 ? () => setRemoving(o) : undefined}\n />\n )}\n </SortableList>\n {options.length < p.maxOptions ? (\n <form\n className=\"flex gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n if (!added.trim()) return;\n act(async () => {\n await editor.addOption(added.trim());\n setAdded(\"\");\n });\n }}\n >\n <Input value={added} placeholder={t(\"pollEditor.newOption\")} aria-label={t(\"pollEditor.newOption\")} onChange={(e) => setAdded(e.target.value)} />\n <Button type=\"submit\" variant=\"outline\" disabled={!added.trim() || p.saving}>\n {t(\"pollEditor.addOption\")}\n </Button>\n </form>\n ) : (\n <p className=\"text-xs text-muted-foreground\">{t(\"pollEditor.maxOptions\", { max: p.maxOptions })}</p>\n )}\n </fieldset>\n )}\n <ConfirmDialog\n open={!!removing}\n onOpenChange={(o) => !o && setRemoving(null)}\n title={t(\"pollEditor.removeOptionTitle\")}\n description={t(\"pollEditor.removeOptionBody\")}\n confirm={t(\"pollEditor.removeOption\")}\n onConfirm={() => removing && act(() => editor.removeOption(removing.id))}\n />\n </div>\n );\n}\n\nconst OPTION_ROW = \"flex flex-wrap items-center gap-2 rounded-lg border border-border p-2\";\n\n/** One option's content: its drag handle, label, image, votes and remove. */\nfunction OptionRow(p: {\n handle: ReactNode;\n index: number;\n label: string;\n /** A new poll's staged image. */\n image?: File | null;\n /** An existing option's image. */\n url?: string;\n votes?: number;\n disabled?: boolean;\n /** A new poll's label as typed. */\n onLabel?: (label: string) => void;\n /** An existing option's label, on blur or Enter. */\n onCommit?: (label: string) => void;\n onImage: (file: File | null) => void;\n onRemove?: () => void;\n}) {\n const m = useMessages();\n const { t } = m;\n const [label, setLabel] = useState(p.label);\n useEffect(() => setLabel(p.label), [p.label]);\n const staged = useObjectURL(p.image ?? null);\n const name = t(\"pollEditor.option\", { index: p.index + 1 });\n return (\n <>\n {p.handle}\n <Input\n className=\"min-w-40 flex-1\"\n value={label}\n aria-label={name}\n placeholder={name}\n disabled={p.disabled && !p.onLabel}\n onChange={(e) => {\n setLabel(e.target.value);\n p.onLabel?.(e.target.value);\n }}\n onBlur={() => p.onCommit?.(label)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && p.onCommit) {\n e.preventDefault();\n p.onCommit(label);\n }\n }}\n />\n <ImageField compact label={t(\"pollEditor.optionImage\", { index: p.index + 1 })} url={p.url ?? staged} disabled={p.disabled} onPick={p.onImage} onRemove={() => p.onImage(null)} />\n {p.votes !== undefined && <span className=\"text-xs tabular-nums text-muted-foreground\">{m.plural(\"poll.votes\", p.votes)}</span>}\n {p.onRemove && (\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={`${t(\"pollEditor.removeOption\")}: ${name}`} disabled={p.disabled} onClick={p.onRemove}>\n <HugeiconsIcon icon={Delete02Icon} strokeWidth={2} />\n </Button>\n )}\n </>\n );\n}\n\nfunction ImageField(p: { label: string; url?: string | null; compact?: boolean; disabled?: boolean; onPick: (file: File) => void; onRemove: () => void }) {\n const { t } = useMessages();\n const input = useRef<HTMLInputElement>(null);\n const pick = (\n <input\n ref={input}\n type=\"file\"\n accept=\"image/*\"\n hidden\n aria-label={p.label}\n onChange={(e) => {\n const f = e.target.files?.[0];\n e.target.value = \"\";\n if (f) p.onPick(f);\n }}\n />\n );\n if (p.compact) {\n return (\n <div className=\"flex items-center gap-1\" data-ckui=\"poll-image\">\n {pick}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon\"\n className=\"overflow-hidden p-0\"\n aria-label={`${p.url ? t(\"pollEditor.replaceImage\") : t(\"pollEditor.uploadImage\")}: ${p.label}`}\n disabled={p.disabled}\n onClick={() => input.current?.click()}\n >\n {p.url ? <img src={p.url} alt=\"\" className=\"size-full object-cover\" /> : <HugeiconsIcon icon={ImageAdd01Icon} strokeWidth={2} />}\n </Button>\n {p.url && (\n <Button type=\"button\" variant=\"ghost\" size=\"xs\" disabled={p.disabled} onClick={p.onRemove}>\n {t(\"pollEditor.removeImage\")}\n </Button>\n )}\n </div>\n );\n }\n return (\n <div className=\"grid gap-1.5\" data-ckui=\"poll-image\">\n <span className=\"text-sm font-medium\">{p.label}</span>\n {pick}\n {p.url && <img src={p.url} alt=\"\" className=\"aspect-video w-full max-w-sm rounded-lg border border-border object-cover\" />}\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={p.disabled} onClick={() => input.current?.click()}>\n <HugeiconsIcon icon={ImageAdd01Icon} strokeWidth={2} data-icon=\"inline-start\" />\n {p.url ? t(\"pollEditor.replaceImage\") : t(\"pollEditor.uploadImage\")}\n </Button>\n {p.url && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" disabled={p.disabled} onClick={p.onRemove}>\n {t(\"pollEditor.removeImage\")}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\nfunction Field({ id, label, children }: { id: string; label: string; children: ReactNode }) {\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n {children}\n </div>\n );\n}\n\n/** An object URL for a staged file, revoked when it changes. */\nfunction useObjectURL(file: File | null): string | undefined {\n const [url, setUrl] = useState<string>();\n useEffect(() => {\n if (!file) return setUrl(undefined);\n const u = URL.createObjectURL(file);\n setUrl(u);\n return () => URL.revokeObjectURL(u);\n }, [file]);\n return url;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiDA,IAAM,KAAS,MAAmB,IAAQ,IAAI,KAAK,CAAK,CAAC,CAAC,YAAY,IAAI,KAAA,GACtE,IAAM,GACJ,WAAsB;CAAE,KAAK,IAAI,EAAE;CAAO,OAAO;CAAI,OAAO;AAAK;AASvE,SAAgB,EAAW,GAAoB;CAC7C,IAAM,IAAI,EAAY,GAChB,EAAE,SAAM,GACR,IAAK,EAAc,EAAE,QAAQ,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,GACvD,IAAS,EAAiB,EAAE,OAAO,GACnC,CAAC,GAAO,KAAY,EAA0C,IAAI,GAClE,CAAC,GAAQ,KAAa,EAAwB,IAAI,GAClD,CAAC,GAAU,KAAe,EAAS,EAAK,GACxC,KAAQ,MAAe;EAC3B,IAAM,IAAM,EAAkB,CAAC;EAE/B,AADA,EAAS,CAAG,GACZ,EAAO,GAAK,WAAW;CACzB,GACM,KAAQ,MACZ,kBAAC,GAAD;EAAkB,YAAY,EAAE;EAAY,WAAW,EAAG,sBAAsB,EAAE,SAAS;EAAG,aAAU;EACrG;CACe,CAAA;CAWpB,OATI,EAAG,WAAW,CAAC,EAAG,OACb,EACL,kBAAC,OAAD;EAAK,WAAU;EAAa,aAAA;EAA5B,UAAA,CACE,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA,GAClC,kBAAC,GAAD,EAAU,WAAU,cAAe,CAAA,CAChC;CACP,CAAA,CAAA,IAEE,EAAG,SAAS,CAAC,EAAG,OAAa,EAAK,kBAAC,GAAD,EAAW,OAAO,EAAG,MAAQ,CAAA,CAAC,IAC7D,EACL,kBAAA,GAAA,EAAA,UAAA;EACE,kBAAC,MAAD;GAAI,WAAU;GAA2B,UAAA,EAAE,EAAG,OAAO,yBAAyB,qBAAqB;EAAM,CAAA;EACxG,EAAG,OACF,kBAAC,GAAD;GAEE,MAAM,EAAG;GACT,QAAQ,EAAG;GACX,YAAY,EAAE,cAAc;GAC5B,QAAQ;GACR,SAAS;GACT,YAAY,MAAQ,EAAS,CAAG;GAChC,UAAU,MAAS;IAGjB,AAFA,EAAS,IAAI,GACb,EAAU,EAAE,kBAAkB,CAAC,GAC/B,EAAE,UAAU,CAAI;GAClB;EACD,GAZM,EAAG,KAAK,EAYd,IAED,kBAAC,GAAD;GACE,QAAQ,EAAG;GACX,WAAW,EAAE;GACb,iBAAiB,EAAE;GACnB,UAAU,CAAC,CAAC,EAAE;GACd,YAAY,EAAE,cAAc;GAC5B,YAAY,MAAQ,EAAS,CAAG;GAChC,UAAU,OAAO,GAAO,MAAW;IACjC,EAAS,IAAI;IACb,IAAI;KACF,IAAM,EAAE,SAAM,mBAAgB,MAAM,EAAG,OAAO,GAAO,CAAM;KAC3D,EAAU,EAAY,SAAS,EAAE,wBAAwB,IAAI,EAAE,oBAAoB,CAAC;KACpF,KAAK,IAAM,KAAK,GAAa,EAAO,GAAG,WAAW;KAClD,EAAE,YAAY,CAAI;IACpB,SAAS,GAAG;KACV,EAAK,CAAC;IACR;GACF;EACD,CAAA;EAEF,KAAS,kBAAC,GAAD,EAAA,UAAY,OAAO,KAAU,WAAW,IAAQ,EAAa,GAAG,CAAK,EAAa,CAAA;EAC3F,KAAU,CAAC,KACV,kBAAC,KAAD;GAAG,MAAK;GAAS,WAAU;GACxB,UAAA;EACA,CAAA;EAEJ,EAAG,QACF,kBAAC,OAAD;GAAK,WAAU;GACb,UAAA,kBAAC,GAAD;IAAQ,SAAQ;IAAc,eAAe,EAAY,EAAI;IAAG,UAAU,EAAG;IAC1E,UAAA,EAAE,mBAAmB;GAChB,CAAA;EACL,CAAA;EAEP,kBAAC,GAAD;GACE,MAAM;GACN,cAAc;GACd,OAAO,EAAE,wBAAwB;GACjC,aAAa,EAAE,uBAAuB;GACtC,SAAS,EAAE,mBAAmB;GAC9B,iBACE,KAAK,EAAG,OAAO,CAAC,CAAC,WACT,EAAE,YAAY,IACnB,MAAM,EAAK,CAAC,CACf;EAEH,CAAA;CACD,EAAA,CAAA,CACJ;AACF;AAEA,SAAS,EAAa,GAQnB;CACD,IAAM,EAAE,SAAM,EAAY,GACpB,IAAM,EAAM,GACZ,IAAQ,SAAe,EAAE,aAAa,CAAC,EAAA,CAAG,KAAK,MAAO,OAAO,KAAM,WAAW;EAAE,OAAO;EAAG,OAAO;CAAE,IAAI,CAAE,GAAG,CAAC,EAAE,SAAS,CAAC,GACzH,CAAC,GAAU,KAAe,EAAS,EAAE,GACrC,CAAC,GAAU,KAAe,EAAS,EAAE,mBAAmB,EAAM,EAAE,EAAE,SAAS,IAAI,GAC/E,CAAC,GAAM,KAAW,EAAmB,iBAAiB,GACtD,CAAC,GAAQ,KAAa,QAAe,kBAAW,IAAI,KAAK,CAAC,CAAC,GAC3D,CAAC,GAAU,KAAe,EAAS,EAAE,GACrC,CAAC,GAAO,KAAY,EAAsB,IAAI,GAC9C,CAAC,GAAS,KAAc,QAAwB,CAAC,EAAM,GAAG,EAAM,CAAC,CAAC,GAClE,KAAO,GAAa,MAA0B,GAAY,MAAO,EAAG,KAAK,MAAO,EAAE,QAAQ,IAAM;EAAE,GAAG;EAAG,GAAG;CAAM,IAAI,CAAE,CAAC,GACxH,KAAQ,GAAc,MAC1B,GAAY,MAAO;EACjB,IAAM,IAAO,CAAC,GAAG,CAAE;EAEnB,OADA,EAAK,OAAO,GAAI,GAAG,GAAG,EAAK,OAAO,GAAM,CAAC,CAAC,GACnC;CACT,CAAC,GACG,UAAe;EACnB,IAAI,CAAC,EAAS,KAAK,GAAG,OAAO,EAAE,UAAU,EAAE,6BAA6B,CAAC;EACzE,IAAM,IAAO,EAAQ,QAAQ,MAAM,EAAE,MAAM,KAAK,KAAK,EAAE,KAAK;EAC5D,IAAI,MAAS,mBAAmB;GAC9B,IAAI,EAAK,MAAM,MAAM,CAAC,EAAE,MAAM,KAAK,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,2BAA2B,CAAC;GACxF,IAAI,EAAK,SAAS,GAAG,OAAO,EAAE,UAAU,EAAE,uBAAuB,CAAC;EACpE;EACA,IAAM,IAAS,MAAS;EACxB,EAAO,SACL;GACE;GACA,UAAU,EAAS,KAAK;GACxB;GACA,SAAS,EAAM,CAAM;GACrB,WAAW,EAAM,CAAQ;GACzB,SAAS,IAAS,EAAK,KAAK,GAAG,OAAc;IAAE,OAAO,EAAE,MAAM,KAAK;IAAG;GAAS,EAAE,IAAI,KAAA;EACvF,GACA;GAAE,UAAU;GAAO,SAAS,IAAS,EAAK,KAAK,MAAM,EAAE,KAAK,IAAI,CAAC;EAAE,CACrE;CACF;CACA,OACE,kBAAC,QAAD;EACE,WAAU;EACV,WAAW,MAAM;GAEf,AADA,EAAE,eAAe,GACjB,EAAO;EACT;EALF,UAAA;GAOE,kBAAC,GAAD;IAAO,IAAI,GAAG,EAAI;IAAK,OAAO,EAAE,qBAAqB;IACnD,UAAA,kBAAC,GAAD;KAAU,IAAI,GAAG,EAAI;KAAK,MAAM;KAAG,OAAO;KAAU,aAAa,EAAE,gCAAgC;KAAG,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;IAAI,CAAA;GAChJ,CAAA;GACP,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,kBAAC,GAAD;MAAO,IAAI,GAAG,EAAI;MAAQ,OAAO,EAAE,qBAAqB;MACrD,UAAA,EAAM,SACL,kBAAC,GAAD;OAAQ,OAAO;OAAU,gBAAgB,MAAM,EAAY,OAAO,CAAC,CAAC;OAApE,UAAA,CACE,kBAAC,GAAD;QAAe,IAAI,GAAG,EAAI;QAAQ,WAAU;QAC1C,UAAA,kBAAC,GAAD,EAAA,WAAe,MAAc,EAAM,MAAM,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,SAAS,EAAe,CAAA;OAC1E,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,EAAM,KAAK,MACV,kBAAC,GAAD;QAA0B,OAAO,EAAE;QAChC,UAAA,EAAE;OACO,GAFK,EAAE,KAEP,CACb,EACY,CAAA,CACT;MAER,CAAA,IAAA,kBAAC,GAAD;OAAO,IAAI,GAAG,EAAI;OAAQ,OAAO;OAAU,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;MAAI,CAAA;KAEvF,CAAA;KACN,EAAE,YACD,kBAAC,YAAD;MAAU,WAAU;MAApB,UAAA,CACE,kBAAC,UAAD;OAAQ,WAAU;OAA8B,UAAA,EAAE,iBAAiB;MAAU,CAAA,GAC7E,kBAAC,OAAD;OAAK,WAAU;OACX,UAAA,CAAC,mBAAmB,WAAW,CAAC,CAAW,KAAK,MAChD,kBAAC,GAAD;QAAgB,MAAK;QAAS,MAAK;QAAK,SAAS,MAAS,IAAI,YAAY;QAAW,gBAAc,MAAS;QAAG,eAAe,EAAQ,CAAC;QACpI,UAAA,EAAE,MAAM,cAAc,wBAAwB,2BAA2B;OACpE,GAFK,CAEL,CACT;MACE,CAAA,CACG;;KAEZ,kBAAC,GAAD;MAAO,IAAI,GAAG,EAAI;MAAQ,OAAO,EAAE,mBAAmB;MACpD,UAAA,kBAAC,GAAD;OAAO,IAAI,GAAG,EAAI;OAAQ,MAAK;OAAiB,OAAO;OAAQ,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;MAAI,CAAA;KACvG,CAAA;KACP,kBAAC,GAAD;MAAO,IAAI,GAAG,EAAI;MAAU,OAAO,EAAE,qBAAqB;MACxD,UAAA,kBAAC,GAAD;OAAO,IAAI,GAAG,EAAI;OAAU,MAAK;OAAiB,OAAO;OAAU,KAAK;OAAQ,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;MAAI,CAAA;KAC1H,CAAA;IACJ;;GACL,kBAAC,GAAD;IAAY,OAAO,EAAE,kBAAkB;IAAG,KAAK,EAAa,CAAK;IAAG,QAAQ;IAAU,gBAAgB,EAAS,IAAI;GAAI,CAAA;GACtH,MAAS,cACR,kBAAC,KAAD;IAAG,WAAU;IAAyB,UAAA,EAAE,yBAAyB;GAAK,CAAA,IAEtE,kBAAC,YAAD;IAAU,WAAU;IAApB,UAAA;KACE,kBAAC,UAAD;MAAQ,WAAU;MAA8B,UAAA,EAAE,oBAAoB;KAAU,CAAA;KAChF,kBAAC,GAAD;MACE,OAAO;MACP,KAAK,MAAM,EAAE;MACb,OAAO,MAAM,EAAE,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;MAChF,QAAQ;MACR,OAAO,EAAE,oBAAoB;MAC7B,WAAU;MACV,YAAY;OAAE,WAAW;OAAY,aAAa;MAAc;MAE9D,WAAA,GAAG,GAAQ,MACX,kBAAC,GAAD;OACU;OACR,OAAO;OACP,OAAO,EAAE;OACT,OAAO,EAAE;OACT,UAAU,MAAU,EAAI,EAAE,KAAK,EAAE,SAAM,CAAC;OACxC,UAAU,MAAU,EAAI,EAAE,KAAK,EAAE,SAAM,CAAC;OACxC,UAAU,EAAQ,SAAS,UAAU,GAAY,MAAO,EAAG,QAAQ,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,IAAI,KAAA;MAC9F,CAAA;KAES,CAAA;KACb,EAAQ,SAAS,EAAE,aAClB,kBAAC,GAAD;MAAQ,MAAK;MAAS,SAAQ;MAAU,MAAK;MAAK,WAAU;MAAqB,eAAe,GAAY,MAAO,CAAC,GAAG,GAAI,EAAM,CAAC,CAAC;MAChI,UAAA,EAAE,sBAAsB;KACnB,CAAA,IAER,kBAAC,KAAD;MAAG,WAAU;MAAiC,UAAA,EAAE,yBAAyB,EAAE,KAAK,EAAE,WAAW,CAAC;KAAK,CAAA;IAE7F;;GAEZ,kBAAC,GAAD;IAAQ,MAAK;IAAS,WAAU;IAAqB,UAAU,EAAE;IAC9D,UAAA,EAAE,SAAS,EAAE,qBAAqB,IAAI,EAAE,mBAAmB;GACtD,CAAA;EACJ;;AAEV;AAEA,SAAS,EAAW,GAQjB;CACD,IAAM,IAAI,EAAY,GAChB,EAAE,SAAM,GACR,IAAM,EAAM,GACZ,EAAE,SAAM,cAAW,GACnB,CAAC,GAAU,KAAe,EAAS,EAAK,QAAQ,GAChD,CAAC,GAAQ,KAAa,EAAS,EAAW,IAAI,KAAK,EAAK,OAAO,CAAC,CAAC,GACjE,CAAC,GAAU,KAAe,EAAS,EAAK,YAAY,EAAW,IAAI,KAAK,EAAK,SAAS,CAAC,IAAI,EAAE,GAC7F,CAAC,GAAQ,KAAa,EAAS,EAAK,SAAS,GAC7C,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAU,KAAe,EAA4B,IAAI,GAC1D,IAAU,CAAC,GAAG,EAAK,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ,GAClE,IAAoB,CAAC;CAK3B,AAJI,EAAS,KAAK,MAAM,EAAK,aAAU,EAAM,WAAW,EAAS,KAAK,IAClE,EAAM,CAAM,MAAM,IAAI,KAAK,EAAW,IAAI,KAAK,EAAK,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,MAAG,EAAM,UAAU,EAAM,CAAM,IAC1G,KAAY,EAAM,CAAQ,OAAO,EAAK,aAAa,IAAI,KAAK,EAAW,IAAI,KAAK,EAAK,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,KAAI,EAAM,YAAY,EAAM,CAAQ,IAC7I,CAAC,KAAY,EAAK,cAAW,EAAM,YAAY,OACpD,MAAW,EAAK,cAAW,EAAM,YAAY;CACjD,IAAM,IAAQ,OAAO,KAAK,CAAK,CAAC,CAAC,SAAS,GACpC,KAAO,MAA+B,KAAK,EAAG,CAAC,CAAC,MAAM,EAAE,OAAO;CACrE,OACE,kBAAC,OAAD;EAAK,WAAU;EAAf,UAAA;GACE,kBAAC,QAAD;IACE,WAAU;IACV,WAAW,MAAM;KAEf,IADA,EAAE,eAAe,GACb,CAAC,EAAS,KAAK,GAAG,OAAO,EAAE,UAAU,EAAE,6BAA6B,CAAC;KACzE,AAAI,KAAO,EAAI,YAAY,EAAE,QAAQ,MAAM,EAAO,OAAO,CAAK,CAAC,CAAC;IAClE;IANF,UAAA;KAQE,kBAAC,GAAD;MAAO,IAAI,GAAG,EAAI;MAAK,OAAO,EAAE,qBAAqB;MACnD,UAAA,kBAAC,GAAD;OAAU,IAAI,GAAG,EAAI;OAAK,MAAM;OAAG,OAAO;OAAU,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;MAAI,CAAA;KAC9F,CAAA;KACP,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAAC,GAAD;OAAO,IAAI,GAAG,EAAI;OAAQ,OAAO,EAAE,mBAAmB;OACpD,UAAA,kBAAC,GAAD;QAAO,IAAI,GAAG,EAAI;QAAQ,MAAK;QAAiB,OAAO;QAAQ,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;OAAI,CAAA;MACvG,CAAA,GACP,kBAAC,GAAD;OAAO,IAAI,GAAG,EAAI;OAAU,OAAO,EAAE,qBAAqB;OACxD,UAAA,kBAAC,GAAD;QAAO,IAAI,GAAG,EAAI;QAAU,MAAK;QAAiB,OAAO;QAAU,KAAK;QAAQ,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;OAAI,CAAA;MAC1H,CAAA,CACJ;;KACL,kBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,kBAAC,GAAD;OAAQ,IAAI,GAAG,EAAI;OAAU,SAAS;OAAQ,iBAAiB;MAAY,CAAA,GAC3E,kBAAC,GAAD;OAAO,SAAS,GAAG,EAAI;OAAW,UAAA,EAAE,IAAS,sBAAsB,qBAAqB;MAAS,CAAA,CAC9F;;KACL,kBAAC,GAAD;MAAQ,MAAK;MAAS,WAAU;MAAqB,UAAU,CAAC,KAAS,EAAE;MACxE,UAAA,EAAE,SAAS,EAAE,eAAe,IAAI,EAAE,aAAa;KAC1C,CAAA;IACJ;;GACN,kBAAC,GAAD;IAAY,OAAO,EAAE,kBAAkB;IAAG,KAAK,EAAK;IAAW,UAAU,EAAE;IAAQ,SAAS,MAAM,QAAU,EAAO,SAAS,CAAC,CAAC;IAAG,gBAAgB,QAAU,EAAO,SAAS,IAAI,CAAC;GAAI,CAAA;GACnL,EAAK,SAAS,cACb,kBAAC,KAAD;IAAG,WAAU;IAAb,UAAA;KACG,EAAE,yBAAyB;KAAE;KAAE,EAAE,OAAO,gBAAgB,EAAK,gBAAgB,CAAC;IAC9E;GAEH,CAAA,IAAA,kBAAC,YAAD;IAAU,WAAU;IAApB,UAAA;KACE,kBAAC,UAAD;MAAQ,WAAU;MAA8B,UAAA,EAAE,oBAAoB;KAAU,CAAA;KAChF,kBAAC,GAAD;MACE,OAAO;MACP,KAAK,MAAM,EAAE;MACb,OAAO,MAAM,EAAE;MACf,UAAU,EAAE;MACZ,SAAS,GAAM,MAAO,QAAU,EAAO,WAAW,EAAQ,EAAK,CAAE,IAAI,CAAE,CAAC;MACxE,OAAO,EAAE,oBAAoB;MAC7B,WAAU;MACV,YAAY;OAAE,WAAW;OAAY,aAAa;MAAc;MAE9D,WAAA,GAAG,GAAQ,MACX,kBAAC,GAAD;OACU;OACR,OAAO;OACP,OAAO,EAAE;OACT,KAAK,EAAE;OACP,OAAO,EAAE;OACT,UAAU,EAAE;OACZ,WAAW,MAAU,EAAM,KAAK,KAAK,EAAM,KAAK,MAAM,EAAE,SAAS,QAAU,EAAO,aAAa,EAAE,IAAI,EAAM,KAAK,CAAC,CAAC;OAClH,UAAU,MAAM,QAAU,EAAO,eAAe,EAAE,IAAI,CAAC,CAAC;OACxD,UAAU,EAAQ,SAAS,UAAU,EAAY,CAAC,IAAI,KAAA;MACvD,CAAA;KAES,CAAA;KACb,EAAQ,SAAS,EAAE,aAClB,kBAAC,QAAD;MACE,WAAU;MACV,WAAW,MAAM;OACf,EAAE,eAAe,GACZ,EAAM,KAAK,KAChB,EAAI,YAAY;QAEd,AADA,MAAM,EAAO,UAAU,EAAM,KAAK,CAAC,GACnC,EAAS,EAAE;OACb,CAAC;MACH;MATF,UAAA,CAWE,kBAAC,GAAD;OAAO,OAAO;OAAO,aAAa,EAAE,sBAAsB;OAAG,cAAY,EAAE,sBAAsB;OAAG,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;MAAI,CAAA,GAChJ,kBAAC,GAAD;OAAQ,MAAK;OAAS,SAAQ;OAAU,UAAU,CAAC,EAAM,KAAK,KAAK,EAAE;OAClE,UAAA,EAAE,sBAAsB;MACnB,CAAA,CACJ;KAEN,CAAA,IAAA,kBAAC,KAAD;MAAG,WAAU;MAAiC,UAAA,EAAE,yBAAyB,EAAE,KAAK,EAAE,WAAW,CAAC;KAAK,CAAA;IAE7F;;GAEZ,kBAAC,GAAD;IACE,MAAM,CAAC,CAAC;IACR,eAAe,MAAM,CAAC,KAAK,EAAY,IAAI;IAC3C,OAAO,EAAE,8BAA8B;IACvC,aAAa,EAAE,6BAA6B;IAC5C,SAAS,EAAE,yBAAyB;IACpC,iBAAiB,KAAY,QAAU,EAAO,aAAa,EAAS,EAAE,CAAC;GACxE,CAAA;EACE;;AAET;AAEA,IAAM,IAAa;AAGnB,SAAS,EAAU,GAgBhB;CACD,IAAM,IAAI,EAAY,GAChB,EAAE,SAAM,GACR,CAAC,GAAO,KAAY,EAAS,EAAE,KAAK;CAC1C,QAAgB,EAAS,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,CAAC;CAC5C,IAAM,IAAS,EAAa,EAAE,SAAS,IAAI,GACrC,IAAO,EAAE,qBAAqB,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;CAC1D,OACE,kBAAA,GAAA,EAAA,UAAA;EACG,EAAE;EACH,kBAAC,GAAD;GACE,WAAU;GACV,OAAO;GACP,cAAY;GACZ,aAAa;GACb,UAAU,EAAE,YAAY,CAAC,EAAE;GAC3B,WAAW,MAAM;IAEf,AADA,EAAS,EAAE,OAAO,KAAK,GACvB,EAAE,UAAU,EAAE,OAAO,KAAK;GAC5B;GACA,cAAc,EAAE,WAAW,CAAK;GAChC,YAAY,MAAM;IAChB,AAAI,EAAE,QAAQ,WAAW,EAAE,aACzB,EAAE,eAAe,GACjB,EAAE,SAAS,CAAK;GAEpB;EACD,CAAA;EACD,kBAAC,GAAD;GAAY,SAAA;GAAQ,OAAO,EAAE,0BAA0B,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;GAAG,KAAK,EAAE,OAAO;GAAQ,UAAU,EAAE;GAAU,QAAQ,EAAE;GAAS,gBAAgB,EAAE,QAAQ,IAAI;EAAI,CAAA;EAChL,EAAE,UAAU,KAAA,KAAa,kBAAC,QAAD;GAAM,WAAU;GAA8C,UAAA,EAAE,OAAO,cAAc,EAAE,KAAK;EAAQ,CAAA;EAC7H,EAAE,YACD,kBAAC,GAAD;GAAQ,MAAK;GAAS,SAAQ;GAAQ,MAAK;GAAU,cAAY,GAAG,EAAE,yBAAyB,EAAE,IAAI;GAAQ,UAAU,EAAE;GAAU,SAAS,EAAE;GAC5I,UAAA,kBAAC,GAAD;IAAe,MAAM;IAAc,aAAa;GAAI,CAAA;EAC9C,CAAA;CAEV,EAAA,CAAA;AAEN;AAEA,SAAS,EAAW,GAAsI;CACxJ,IAAM,EAAE,SAAM,EAAY,GACpB,IAAQ,EAAyB,IAAI,GACrC,IACJ,kBAAC,SAAD;EACE,KAAK;EACL,MAAK;EACL,QAAO;EACP,QAAA;EACA,cAAY,EAAE;EACd,WAAW,MAAM;GACf,IAAM,IAAI,EAAE,OAAO,QAAQ;GAE3B,AADA,EAAE,OAAO,QAAQ,IACb,KAAG,EAAE,OAAO,CAAC;EACnB;CACD,CAAA;CAyBH,OAvBI,EAAE,UAEF,kBAAC,OAAD;EAAK,WAAU;EAA0B,aAAU;EAAnD,UAAA;GACG;GACD,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,MAAK;IACL,WAAU;IACV,cAAY,GAAG,EAAE,MAAM,EAAE,yBAAyB,IAAI,EAAE,wBAAwB,EAAE,IAAI,EAAE;IACxF,UAAU,EAAE;IACZ,eAAe,EAAM,SAAS,MAAM;IAEnC,UAAA,EAAE,MAAM,kBAAC,OAAD;KAAK,KAAK,EAAE;KAAK,KAAI;KAAG,WAAU;IAA0B,CAAA,IAAI,kBAAC,GAAD;KAAe,MAAM;KAAgB,aAAa;IAAI,CAAA;GACzH,CAAA;GACP,EAAE,OACD,kBAAC,GAAD;IAAQ,MAAK;IAAS,SAAQ;IAAQ,MAAK;IAAK,UAAU,EAAE;IAAU,SAAS,EAAE;IAC9E,UAAA,EAAE,wBAAwB;GACrB,CAAA;EAEP;MAIP,kBAAC,OAAD;EAAK,WAAU;EAAe,aAAU;EAAxC,UAAA;GACE,kBAAC,QAAD;IAAM,WAAU;IAAuB,UAAA,EAAE;GAAY,CAAA;GACpD;GACA,EAAE,OAAO,kBAAC,OAAD;IAAK,KAAK,EAAE;IAAK,KAAI;IAAG,WAAU;GAA6E,CAAA;GACzH,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,GAAD;KAAQ,MAAK;KAAS,SAAQ;KAAU,MAAK;KAAK,UAAU,EAAE;KAAU,eAAe,EAAM,SAAS,MAAM;KAA5G,UAAA,CACE,kBAAC,GAAD;MAAe,MAAM;MAAgB,aAAa;MAAG,aAAU;KAAgB,CAAA,GAC9E,EAAE,MAAM,EAAE,yBAAyB,IAAI,EAAE,wBAAwB,CAC5D;IACP,CAAA,GAAA,EAAE,OACD,kBAAC,GAAD;KAAQ,MAAK;KAAS,SAAQ;KAAQ,MAAK;KAAK,UAAU,EAAE;KAAU,SAAS,EAAE;KAC9E,UAAA,EAAE,wBAAwB;IACrB,CAAA,CAEP;;EACF;;AAET;AAEA,SAAS,EAAM,EAAE,OAAI,UAAO,eAAgE;CAC1F,OACE,kBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,kBAAC,GAAD;GAAO,SAAS;GAAK,UAAA;EAAa,CAAA,GACjC,CACE;;AAET;AAGA,SAAS,EAAa,GAAuC;CAC3D,IAAM,CAAC,GAAK,KAAU,EAAiB;CAOvC,OANA,QAAgB;EACd,IAAI,CAAC,GAAM,OAAO,EAAO,KAAA,CAAS;EAClC,IAAM,IAAI,IAAI,gBAAgB,CAAI;EAElC,OADA,EAAO,CAAC,SACK,IAAI,gBAAgB,CAAC;CACpC,GAAG,CAAC,CAAI,CAAC,GACF;AACT"}
@@ -51,7 +51,7 @@ export interface UseAdminComments extends UseList<AdminComment> {
51
51
  restore: (id: string) => Promise<void>;
52
52
  react: (comment: Comment, value: Reaction) => Promise<void>;
53
53
  }
54
- /** Staff: every comment, newest first, deleted, held and rejected ones with their bodies (CommentModerate). */
54
+ /** Staff: every comment, newest first, deleted, held and rejected ones with their bodies (root:comments:moderate). */
55
55
  export declare function useAdminComments(o?: {
56
56
  contentKind?: string;
57
57
  pageSize?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"comments.js","names":[],"sources":["../../src/react/comments.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport type { BanScope, Decision, HeldKind, Reaction, Sort } from \"../client/content/types.js\";\nimport type { AdminComment, BanInput, Comment, CommentBan, CommentStanding, FeedItem, HeldItem, ReactionCounts, RefBody } from \"../client/generated/wire.js\";\nimport type { Page, ResourceStore } from \"./resources.js\";\nimport { keyOf, offsetPages, useContentScope, useList, useResource, type UseList } from \"./use-resource.js\";\n\n/** Counts after the caller's reaction changes from counts.mine to value. */\nexport function withReaction(counts: ReactionCounts, value: Reaction): ReactionCounts {\n const likes = counts.likes - (counts.mine === 1 ? 1 : 0) + (value === 1 ? 1 : 0);\n const dislikes = counts.dislikes - (counts.mine === -1 ? 1 : 0) + (value === -1 ? 1 : 0);\n return { likes, dislikes, mine: value };\n}\n\nconst commentLists = [\"comments\", \"replies\", \"latest\", \"admin-comments\"];\nconst tokens = new WeakMap<ResourceStore, Map<string, number>>();\n\n/** Sets the caller's reaction to a comment in every list showing it at once; rolls back on failure unless a newer one started. */\nasync function reactToComment(client: ContentKitClient, store: ResourceStore, scope: string, c: Pick<Comment, \"id\" | \"likes\" | \"dislikes\" | \"mine\">, value: Reaction) {\n let t = tokens.get(store);\n if (!t) tokens.set(store, (t = new Map()));\n const token = (t.get(c.id) ?? 0) + 1;\n t.set(c.id, token);\n const reads = new Set<number>();\n const put = (counts: ReactionCounts, rollback = false) =>\n store.patch<Page<Comment>>(\n (tag) => commentLists.includes(tag.type) && tag.readScope === scope,\n (page, readVersion) => {\n if (rollback && !reads.has(readVersion)) return page;\n reads.add(readVersion);\n return { ...page, items: page.items.map((x) => (x.id === c.id ? { ...x, ...counts } : x)) };\n },\n );\n put(withReaction({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, value));\n try {\n await client.comments.react(c.id, value);\n } catch (e) {\n if (t.get(c.id) === token) put({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, true);\n throw e;\n }\n}\n\nexport interface UseCommentsOptions {\n sort?: Sort;\n /** Top-level comments per page. Default 20. */\n pageSize?: number;\n client?: ContentKitClient;\n}\n\nexport interface UseComments extends UseList<Comment> {\n /** Comments on the item, or replies to a top-level comment (replyTo); anonymous callers give anonName. */\n post: (body: string, o?: { replyTo?: string; anonName?: string }) => Promise<Comment>;\n edit: (id: string, body: string) => Promise<Comment>;\n /** Leaves a tombstone. */\n remove: (id: string) => Promise<void>;\n /** Sets the caller's reaction at once, rolled back if the server refuses it. */\n react: (comment: Comment, value: Reaction) => Promise<void>;\n /** Restores a deleted comment (moderators). */\n restore: (id: string) => Promise<void>;\n}\n\n/** An item's top-level comments, a page at a time, with every write; new comments and replies land in place. */\nexport function useComments(ref: RefBody, o: UseCommentsOptions = {}): UseComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n const sort = o.sort ?? \"newest\";\n const key = keyOf(\"comments\", scope, ref.kind, ref.id, sort, size);\n const list = useList(store, key, { type: \"comments\", ref: { kind: ref.kind, id: ref.id } }, offsetPages(size, (q, signal) => client.comments.list(ref, { ...q, sort }, signal)));\n const { kind, id } = ref;\n const post = useCallback<UseComments[\"post\"]>(\n (body, p = {}) => client.comments.create({ kind, id }, { body, reply_to_id: p.replyTo, anon_name: p.anonName }),\n [client, kind, id],\n );\n const edit = useCallback((cid: string, body: string) => client.comments.edit(cid, body), [client]);\n const remove = useCallback((cid: string) => client.comments.delete(cid), [client]);\n const restore = useCallback((cid: string) => client.comments.restore(cid), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, post, edit, remove, react, restore };\n}\n\n/** A top-level comment's replies, oldest first, a page at a time; enabled false defers the first read. */\nexport function useCommentReplies(id: string, o: { pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseList<Comment> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 10;\n const key = o.enabled === false ? null : keyOf(\"replies\", scope, id, size);\n return useList(store, key, { type: \"replies\", id }, offsetPages(size, (q, signal) => client.comments.replies(id, q, signal)));\n}\n\n/** Whether the caller may comment on the item, the ban that stops it, and what it may do to others' comments. */\nexport function useCanComment(ref: RefBody, o: { client?: ContentKitClient } = {}) {\n const { client, store, scope } = useContentScope(o.client);\n const r = useResource<CommentStanding>(store, keyOf(\"standing\", scope, ref.kind, ref.id), { type: \"standing\", ref: { kind: ref.kind, id: ref.id } }, (s) =>\n client.comments.standing(ref, s),\n );\n return { standing: r.data ?? null, loading: r.loading || !r.loaded, error: r.error, reload: r.reload };\n}\n\n/** The newest published comments across the site, with their items (the host shows titles). */\nexport function useLatestComments(o: { pageSize?: number; client?: ContentKitClient } = {}): UseList<FeedItem> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n return useList(store, keyOf(\"latest\", scope, size), { type: \"latest\" }, offsetPages(size, (q, signal) => client.comments.latest(q, signal), true));\n}\n\nexport interface UseAdminComments extends UseList<AdminComment> {\n remove: (id: string) => Promise<void>;\n restore: (id: string) => Promise<void>;\n react: (comment: Comment, value: Reaction) => Promise<void>;\n}\n\n/** Staff: every comment, newest first, deleted, held and rejected ones with their bodies (CommentModerate). */\nexport function useAdminComments(o: { contentKind?: string; pageSize?: number; client?: ContentKitClient } = {}): UseAdminComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 25;\n const kind = o.contentKind || undefined;\n const list = useList(\n store,\n keyOf(\"admin-comments\", scope, kind, size),\n { type: \"admin-comments\" },\n offsetPages(size, (q, signal) => client.comments.adminList({ ...q, contentKind: kind }, signal)),\n );\n const remove = useCallback((id: string) => client.comments.delete(id), [client]);\n const restore = useCallback((id: string) => client.comments.restore(id), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, remove, restore, react };\n}\n\nexport interface UseModerationQueue extends UseList<HeldItem> {\n /** Approves (publishes) or rejects at the revision the queue listed; the item leaves the queue. */\n resolve: (item: HeldItem, decision: Decision, reason?: string) => Promise<void>;\n}\n\n/** Staff: held comments or posts awaiting review, oldest first (ModerationReview). */\nexport function useModerationQueue(o: { kind?: HeldKind; pageSize?: number; client?: ContentKitClient } = {}): UseModerationQueue {\n const { client, store, scope } = useContentScope(o.client);\n const kind = o.kind ?? \"comment\";\n const size = o.pageSize ?? 20;\n const list = useList<HeldItem>(store, keyOf(\"held\", scope, kind, size), { type: \"held\" }, async (cursor, signal) => {\n const page = await client.moderation.held({ kind, cursor: typeof cursor === \"string\" ? cursor : undefined, limit: size }, signal);\n return { items: page.items, next: page.next ?? null };\n });\n const resolve = useCallback(async (item: HeldItem, d: Decision, reason?: string) => void (await client.moderation.resolve(item, d, reason)), [client]);\n return { ...list, resolve };\n}\n\nexport interface UseCommentBans extends UseList<CommentBan & { id: string }> {\n ban: (user: string, input?: BanInput) => Promise<CommentBan>;\n lift: (user: string) => Promise<void>;\n}\n\n/** The bans of one scope: the caller's own (\"owner\") or the site's (\"global\", CommentBan), newest first. */\nexport function useCommentBans(o: { scope?: BanScope; pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseCommentBans {\n const { client, store, scope: readScope } = useContentScope(o.client);\n const scope = o.scope ?? \"global\";\n const size = o.pageSize ?? 25;\n const list = useList(\n store,\n o.enabled === false ? null : keyOf(\"bans\", readScope, scope, size),\n { type: \"bans\", scope },\n offsetPages(size, async (q, signal) => (await client.bans.list(scope, q, signal)).map((b) => ({ ...b, id: b.user_id }))),\n );\n const ban = useCallback((user: string, input?: BanInput) => client.bans.ban(scope, user, input), [client, scope]);\n const lift = useCallback((user: string) => client.bans.lift(scope, user), [client, scope]);\n return { ...list, ban, lift };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"comments.js","names":[],"sources":["../../src/react/comments.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport type { BanScope, Decision, HeldKind, Reaction, Sort } from \"../client/content/types.js\";\nimport type { AdminComment, BanInput, Comment, CommentBan, CommentStanding, FeedItem, HeldItem, ReactionCounts, RefBody } from \"../client/generated/wire.js\";\nimport type { Page, ResourceStore } from \"./resources.js\";\nimport { keyOf, offsetPages, useContentScope, useList, useResource, type UseList } from \"./use-resource.js\";\n\n/** Counts after the caller's reaction changes from counts.mine to value. */\nexport function withReaction(counts: ReactionCounts, value: Reaction): ReactionCounts {\n const likes = counts.likes - (counts.mine === 1 ? 1 : 0) + (value === 1 ? 1 : 0);\n const dislikes = counts.dislikes - (counts.mine === -1 ? 1 : 0) + (value === -1 ? 1 : 0);\n return { likes, dislikes, mine: value };\n}\n\nconst commentLists = [\"comments\", \"replies\", \"latest\", \"admin-comments\"];\nconst tokens = new WeakMap<ResourceStore, Map<string, number>>();\n\n/** Sets the caller's reaction to a comment in every list showing it at once; rolls back on failure unless a newer one started. */\nasync function reactToComment(client: ContentKitClient, store: ResourceStore, scope: string, c: Pick<Comment, \"id\" | \"likes\" | \"dislikes\" | \"mine\">, value: Reaction) {\n let t = tokens.get(store);\n if (!t) tokens.set(store, (t = new Map()));\n const token = (t.get(c.id) ?? 0) + 1;\n t.set(c.id, token);\n const reads = new Set<number>();\n const put = (counts: ReactionCounts, rollback = false) =>\n store.patch<Page<Comment>>(\n (tag) => commentLists.includes(tag.type) && tag.readScope === scope,\n (page, readVersion) => {\n if (rollback && !reads.has(readVersion)) return page;\n reads.add(readVersion);\n return { ...page, items: page.items.map((x) => (x.id === c.id ? { ...x, ...counts } : x)) };\n },\n );\n put(withReaction({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, value));\n try {\n await client.comments.react(c.id, value);\n } catch (e) {\n if (t.get(c.id) === token) put({ likes: c.likes, dislikes: c.dislikes, mine: c.mine }, true);\n throw e;\n }\n}\n\nexport interface UseCommentsOptions {\n sort?: Sort;\n /** Top-level comments per page. Default 20. */\n pageSize?: number;\n client?: ContentKitClient;\n}\n\nexport interface UseComments extends UseList<Comment> {\n /** Comments on the item, or replies to a top-level comment (replyTo); anonymous callers give anonName. */\n post: (body: string, o?: { replyTo?: string; anonName?: string }) => Promise<Comment>;\n edit: (id: string, body: string) => Promise<Comment>;\n /** Leaves a tombstone. */\n remove: (id: string) => Promise<void>;\n /** Sets the caller's reaction at once, rolled back if the server refuses it. */\n react: (comment: Comment, value: Reaction) => Promise<void>;\n /** Restores a deleted comment (moderators). */\n restore: (id: string) => Promise<void>;\n}\n\n/** An item's top-level comments, a page at a time, with every write; new comments and replies land in place. */\nexport function useComments(ref: RefBody, o: UseCommentsOptions = {}): UseComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n const sort = o.sort ?? \"newest\";\n const key = keyOf(\"comments\", scope, ref.kind, ref.id, sort, size);\n const list = useList(store, key, { type: \"comments\", ref: { kind: ref.kind, id: ref.id } }, offsetPages(size, (q, signal) => client.comments.list(ref, { ...q, sort }, signal)));\n const { kind, id } = ref;\n const post = useCallback<UseComments[\"post\"]>(\n (body, p = {}) => client.comments.create({ kind, id }, { body, reply_to_id: p.replyTo, anon_name: p.anonName }),\n [client, kind, id],\n );\n const edit = useCallback((cid: string, body: string) => client.comments.edit(cid, body), [client]);\n const remove = useCallback((cid: string) => client.comments.delete(cid), [client]);\n const restore = useCallback((cid: string) => client.comments.restore(cid), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, post, edit, remove, react, restore };\n}\n\n/** A top-level comment's replies, oldest first, a page at a time; enabled false defers the first read. */\nexport function useCommentReplies(id: string, o: { pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseList<Comment> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 10;\n const key = o.enabled === false ? null : keyOf(\"replies\", scope, id, size);\n return useList(store, key, { type: \"replies\", id }, offsetPages(size, (q, signal) => client.comments.replies(id, q, signal)));\n}\n\n/** Whether the caller may comment on the item, the ban that stops it, and what it may do to others' comments. */\nexport function useCanComment(ref: RefBody, o: { client?: ContentKitClient } = {}) {\n const { client, store, scope } = useContentScope(o.client);\n const r = useResource<CommentStanding>(store, keyOf(\"standing\", scope, ref.kind, ref.id), { type: \"standing\", ref: { kind: ref.kind, id: ref.id } }, (s) =>\n client.comments.standing(ref, s),\n );\n return { standing: r.data ?? null, loading: r.loading || !r.loaded, error: r.error, reload: r.reload };\n}\n\n/** The newest published comments across the site, with their items (the host shows titles). */\nexport function useLatestComments(o: { pageSize?: number; client?: ContentKitClient } = {}): UseList<FeedItem> {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 20;\n return useList(store, keyOf(\"latest\", scope, size), { type: \"latest\" }, offsetPages(size, (q, signal) => client.comments.latest(q, signal), true));\n}\n\nexport interface UseAdminComments extends UseList<AdminComment> {\n remove: (id: string) => Promise<void>;\n restore: (id: string) => Promise<void>;\n react: (comment: Comment, value: Reaction) => Promise<void>;\n}\n\n/** Staff: every comment, newest first, deleted, held and rejected ones with their bodies (root:comments:moderate). */\nexport function useAdminComments(o: { contentKind?: string; pageSize?: number; client?: ContentKitClient } = {}): UseAdminComments {\n const { client, store, scope } = useContentScope(o.client);\n const size = o.pageSize ?? 25;\n const kind = o.contentKind || undefined;\n const list = useList(\n store,\n keyOf(\"admin-comments\", scope, kind, size),\n { type: \"admin-comments\" },\n offsetPages(size, (q, signal) => client.comments.adminList({ ...q, contentKind: kind }, signal)),\n );\n const remove = useCallback((id: string) => client.comments.delete(id), [client]);\n const restore = useCallback((id: string) => client.comments.restore(id), [client]);\n const react = useCallback((c: Comment, v: Reaction) => reactToComment(client, store, scope, c, v), [client, store, scope]);\n return { ...list, remove, restore, react };\n}\n\nexport interface UseModerationQueue extends UseList<HeldItem> {\n /** Approves (publishes) or rejects at the revision the queue listed; the item leaves the queue. */\n resolve: (item: HeldItem, decision: Decision, reason?: string) => Promise<void>;\n}\n\n/** Staff: held comments or posts awaiting review, oldest first (ModerationReview). */\nexport function useModerationQueue(o: { kind?: HeldKind; pageSize?: number; client?: ContentKitClient } = {}): UseModerationQueue {\n const { client, store, scope } = useContentScope(o.client);\n const kind = o.kind ?? \"comment\";\n const size = o.pageSize ?? 20;\n const list = useList<HeldItem>(store, keyOf(\"held\", scope, kind, size), { type: \"held\" }, async (cursor, signal) => {\n const page = await client.moderation.held({ kind, cursor: typeof cursor === \"string\" ? cursor : undefined, limit: size }, signal);\n return { items: page.items, next: page.next ?? null };\n });\n const resolve = useCallback(async (item: HeldItem, d: Decision, reason?: string) => void (await client.moderation.resolve(item, d, reason)), [client]);\n return { ...list, resolve };\n}\n\nexport interface UseCommentBans extends UseList<CommentBan & { id: string }> {\n ban: (user: string, input?: BanInput) => Promise<CommentBan>;\n lift: (user: string) => Promise<void>;\n}\n\n/** The bans of one scope: the caller's own (\"owner\") or the site's (\"global\", CommentBan), newest first. */\nexport function useCommentBans(o: { scope?: BanScope; pageSize?: number; enabled?: boolean; client?: ContentKitClient } = {}): UseCommentBans {\n const { client, store, scope: readScope } = useContentScope(o.client);\n const scope = o.scope ?? \"global\";\n const size = o.pageSize ?? 25;\n const list = useList(\n store,\n o.enabled === false ? null : keyOf(\"bans\", readScope, scope, size),\n { type: \"bans\", scope },\n offsetPages(size, async (q, signal) => (await client.bans.list(scope, q, signal)).map((b) => ({ ...b, id: b.user_id }))),\n );\n const ban = useCallback((user: string, input?: BanInput) => client.bans.ban(scope, user, input), [client, scope]);\n const lift = useCallback((user: string) => client.bans.lift(scope, user), [client, scope]);\n return { ...list, ban, lift };\n}\n"],"mappings":";;;AAQA,SAAgB,EAAa,GAAwB,GAAiC;CAGpF,OAAO;EAAE,OAFK,EAAO,QAAS,IAAO,SAAS,KAAc,QAAU;EAEtD,UADC,EAAO,WAAY,IAAO,SAAS,MAAe,QAAU;EACnD,MAAM;CAAM;AACxC;AAEA,IAAM,IAAe;CAAC;CAAY;CAAW;CAAU;AAAgB,GACjE,oBAAS,IAAI,QAA4C;AAG/D,eAAe,EAAe,GAA0B,GAAsB,GAAe,GAAwD,GAAiB;CACpK,IAAI,IAAI,EAAO,IAAI,CAAK;CACxB,AAAK,KAAG,EAAO,IAAI,GAAQ,oBAAI,IAAI,IAAI,CAAE;CACzC,IAAM,KAAS,EAAE,IAAI,EAAE,EAAE,KAAK,KAAK;CACnC,EAAE,IAAI,EAAE,IAAI,CAAK;CACjB,IAAM,oBAAQ,IAAI,IAAY,GACxB,KAAO,GAAwB,IAAW,OAC9C,EAAM,OACH,MAAQ,EAAa,SAAS,EAAI,IAAI,KAAK,EAAI,cAAc,IAC7D,GAAM,MACD,KAAY,CAAC,EAAM,IAAI,CAAW,IAAU,KAChD,EAAM,IAAI,CAAW,GACd;EAAE,GAAG;EAAM,OAAO,EAAK,MAAM,KAAK,MAAO,EAAE,OAAO,EAAE,KAAK;GAAE,GAAG;GAAG,GAAG;EAAO,IAAI,CAAE;CAAE,EAE9F;CACF,EAAI,EAAa;EAAE,OAAO,EAAE;EAAO,UAAU,EAAE;EAAU,MAAM,EAAE;CAAK,GAAG,CAAK,CAAC;CAC/E,IAAI;EACF,MAAM,EAAO,SAAS,MAAM,EAAE,IAAI,CAAK;CACzC,SAAS,GAAG;EAEV,MADI,EAAE,IAAI,EAAE,EAAE,MAAM,KAAO,EAAI;GAAE,OAAO,EAAE;GAAO,UAAU,EAAE;GAAU,MAAM,EAAE;EAAK,GAAG,EAAI,GACrF;CACR;AACF;AAsBA,SAAgB,EAAY,GAAc,IAAwB,CAAC,GAAgB;CACjF,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY,IACrB,IAAO,EAAE,QAAQ,UACjB,IAAM,EAAM,YAAY,GAAO,EAAI,MAAM,EAAI,IAAI,GAAM,CAAI,GAC3D,IAAO,EAAQ,GAAO,GAAK;EAAE,MAAM;EAAY,KAAK;GAAE,MAAM,EAAI;GAAM,IAAI,EAAI;EAAG;CAAE,GAAG,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,KAAK,GAAK;EAAE,GAAG;EAAG;CAAK,GAAG,CAAM,CAAC,CAAC,GACzK,EAAE,SAAM,UAAO,GACf,IAAO,GACV,GAAM,IAAI,CAAC,MAAM,EAAO,SAAS,OAAO;EAAE;EAAM;CAAG,GAAG;EAAE;EAAM,aAAa,EAAE;EAAS,WAAW,EAAE;CAAS,CAAC,GAC9G;EAAC;EAAQ;EAAM;CAAE,CACnB,GACM,IAAO,GAAa,GAAa,MAAiB,EAAO,SAAS,KAAK,GAAK,CAAI,GAAG,CAAC,CAAM,CAAC,GAC3F,IAAS,GAAa,MAAgB,EAAO,SAAS,OAAO,CAAG,GAAG,CAAC,CAAM,CAAC,GAC3E,IAAU,GAAa,MAAgB,EAAO,SAAS,QAAQ,CAAG,GAAG,CAAC,CAAM,CAAC,GAC7E,IAAQ,GAAa,GAAY,MAAgB,EAAe,GAAQ,GAAO,GAAO,GAAG,CAAC,GAAG;EAAC;EAAQ;EAAO;CAAK,CAAC;CACzH,OAAO;EAAE,GAAG;EAAM;EAAM;EAAM;EAAQ;EAAO;CAAQ;AACvD;AAGA,SAAgB,EAAkB,GAAY,IAAyE,CAAC,GAAqB;CAC3I,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY,IACrB,IAAM,EAAE,YAAY,KAAQ,OAAO,EAAM,WAAW,GAAO,GAAI,CAAI;CACzE,OAAO,EAAQ,GAAO,GAAK;EAAE,MAAM;EAAW;CAAG,GAAG,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,QAAQ,GAAI,GAAG,CAAM,CAAC,CAAC;AAC9H;AAGA,SAAgB,EAAc,GAAc,IAAmC,CAAC,GAAG;CACjF,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAI,EAA6B,GAAO,EAAM,YAAY,GAAO,EAAI,MAAM,EAAI,EAAE,GAAG;EAAE,MAAM;EAAY,KAAK;GAAE,MAAM,EAAI;GAAM,IAAI,EAAI;EAAG;CAAE,IAAI,MACpJ,EAAO,SAAS,SAAS,GAAK,CAAC,CACjC;CACA,OAAO;EAAE,UAAU,EAAE,QAAQ;EAAM,SAAS,EAAE,WAAW,CAAC,EAAE;EAAQ,OAAO,EAAE;EAAO,QAAQ,EAAE;CAAO;AACvG;AAGA,SAAgB,EAAkB,IAAsD,CAAC,GAAsB;CAC7G,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY;CAC3B,OAAO,EAAQ,GAAO,EAAM,UAAU,GAAO,CAAI,GAAG,EAAE,MAAM,SAAS,GAAG,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,OAAO,GAAG,CAAM,GAAG,EAAI,CAAC;AACnJ;AASA,SAAgB,EAAiB,IAA4E,CAAC,GAAqB;CACjI,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,YAAY,IACrB,IAAO,EAAE,eAAe,KAAA,GACxB,IAAO,EACX,GACA,EAAM,kBAAkB,GAAO,GAAM,CAAI,GACzC,EAAE,MAAM,iBAAiB,GACzB,EAAY,IAAO,GAAG,MAAW,EAAO,SAAS,UAAU;EAAE,GAAG;EAAG,aAAa;CAAK,GAAG,CAAM,CAAC,CACjG,GACM,IAAS,GAAa,MAAe,EAAO,SAAS,OAAO,CAAE,GAAG,CAAC,CAAM,CAAC,GACzE,IAAU,GAAa,MAAe,EAAO,SAAS,QAAQ,CAAE,GAAG,CAAC,CAAM,CAAC,GAC3E,IAAQ,GAAa,GAAY,MAAgB,EAAe,GAAQ,GAAO,GAAO,GAAG,CAAC,GAAG;EAAC;EAAQ;EAAO;CAAK,CAAC;CACzH,OAAO;EAAE,GAAG;EAAM;EAAQ;EAAS;CAAM;AAC3C;AAQA,SAAgB,EAAmB,IAAuE,CAAC,GAAuB;CAChI,IAAM,EAAE,WAAQ,UAAO,aAAU,EAAgB,EAAE,MAAM,GACnD,IAAO,EAAE,QAAQ,WACjB,IAAO,EAAE,YAAY,IACrB,IAAO,EAAkB,GAAO,EAAM,QAAQ,GAAO,GAAM,CAAI,GAAG,EAAE,MAAM,OAAO,GAAG,OAAO,GAAQ,MAAW;EAClH,IAAM,IAAO,MAAM,EAAO,WAAW,KAAK;GAAE;GAAM,QAAQ,OAAO,KAAW,WAAW,IAAS,KAAA;GAAW,OAAO;EAAK,GAAG,CAAM;EAChI,OAAO;GAAE,OAAO,EAAK;GAAO,MAAM,EAAK,QAAQ;EAAK;CACtD,CAAC,GACK,IAAU,EAAY,OAAO,GAAgB,GAAa,MAAoB,KAAM,MAAM,EAAO,WAAW,QAAQ,GAAM,GAAG,CAAM,GAAI,CAAC,CAAM,CAAC;CACrJ,OAAO;EAAE,GAAG;EAAM;CAAQ;AAC5B;AAQA,SAAgB,EAAe,IAA2F,CAAC,GAAmB;CAC5I,IAAM,EAAE,WAAQ,UAAO,OAAO,MAAc,EAAgB,EAAE,MAAM,GAC9D,IAAQ,EAAE,SAAS,UACnB,IAAO,EAAE,YAAY,IACrB,IAAO,EACX,GACA,EAAE,YAAY,KAAQ,OAAO,EAAM,QAAQ,GAAW,GAAO,CAAI,GACjE;EAAE,MAAM;EAAQ;CAAM,GACtB,EAAY,GAAM,OAAO,GAAG,OAAY,MAAM,EAAO,KAAK,KAAK,GAAO,GAAG,CAAM,EAAA,CAAG,KAAK,OAAO;EAAE,GAAG;EAAG,IAAI,EAAE;CAAQ,EAAE,CAAC,CACzH,GACM,IAAM,GAAa,GAAc,MAAqB,EAAO,KAAK,IAAI,GAAO,GAAM,CAAK,GAAG,CAAC,GAAQ,CAAK,CAAC,GAC1G,IAAO,GAAa,MAAiB,EAAO,KAAK,KAAK,GAAO,CAAI,GAAG,CAAC,GAAQ,CAAK,CAAC;CACzF,OAAO;EAAE,GAAG;EAAM;EAAK;CAAK;AAC9B"}
@@ -32,7 +32,7 @@ export interface PollFilter {
32
32
  month?: string;
33
33
  /** YYYY-MM-DD */
34
34
  date?: string;
35
- /** Staff: every poll, scheduled and inactive ones included (PollWrite). */
35
+ /** Staff: every poll, scheduled and inactive ones included (root:polls:write). */
36
36
  admin?: boolean;
37
37
  pageSize?: number;
38
38
  }
@@ -68,7 +68,7 @@ export interface UsePollEditor {
68
68
  setImage: (file: Blob | null) => Promise<void>;
69
69
  setOptionImage: (option: string, file: Blob | null) => Promise<void>;
70
70
  }
71
- /** Staff: one poll's editor (PollWrite); null creates a new poll. */
71
+ /** Staff: one poll's editor (root:polls:write); null creates a new poll. */
72
72
  export declare function usePollEditor(id: string | null | undefined, o?: {
73
73
  client?: ContentKitClient;
74
74
  }): UsePollEditor;
@@ -1 +1 @@
1
- {"version":3,"file":"polls.js","names":[],"sources":["../../src/react/polls.ts"],"sourcesContent":["import { useCallback, useState } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../client/errors.js\";\nimport type { Poll, PollInput, PollOption, PollUpdate } from \"../client/generated/wire.js\";\nimport { keyOf, offsetPages, useContentScope, useList, useResource, type UseList } from \"./use-resource.js\";\n\n/** The poll after the caller votes for option; a vote is final. */\nexport function withVote(p: Poll, option: string): Poll {\n if (p.voted) return p;\n return { ...p, voted: true, my_option: option, total_votes: p.total_votes + 1, options: p.options.map((o) => (o.id === option ? { ...o, vote_count: o.vote_count + 1 } : o)) };\n}\n\nexport interface UsePoll {\n /** null: no such poll (or no live one). */\n poll: Poll | null;\n loading: boolean;\n error?: ContentKitError;\n reload: () => void;\n /** A vote or answer is in flight. */\n pending: boolean;\n /** Votes at once (a vote is final); rolled back if the server refuses it. */\n vote: (option: string) => Promise<void>;\n /** Stores or replaces the signed-in caller's free-text answer. */\n answer: (text: string) => Promise<void>;\n}\n\n/**\n * A poll with the caller's vote or answer. Without an id it is the newest\n * live poll (in language): the one a home page shows.\n */\nexport function usePoll(id?: string | null, o: { initial?: Poll; language?: string; client?: ContentKitClient } = {}): UsePoll {\n const { client, store, scope } = useContentScope(o.client);\n const key = id ? keyOf(\"poll\", scope, id) : keyOf(\"poll\", scope, \"latest\", o.language ?? \"\");\n const r = useResource<Poll | null>(\n store,\n key,\n { type: \"poll\", id: id ?? undefined },\n async (s) => (id ? client.polls.get(id, s) : ((await client.polls.list({ language: o.language, limit: 1 }, s))[0] ?? null)),\n { initial: o.initial },\n );\n const [pending, setPending] = useState(false);\n const poll = r.data ?? null;\n const vote = useCallback(\n async (option: string) => {\n const prev = store.snapshot<Poll | null>(key).data ?? poll;\n if (!prev) return;\n const token = store.mark(key);\n store.set(key, withVote(prev, option));\n setPending(true);\n try {\n await client.polls.vote(prev.id, option);\n } catch (e) {\n store.rollback(key, token, prev);\n throw toContentKitError(e);\n } finally {\n setPending(false);\n }\n },\n [client, store, key, poll],\n );\n const answer = useCallback(\n async (text: string) => {\n if (!poll) return;\n setPending(true);\n try {\n await client.polls.answer(poll.id, text);\n } finally {\n setPending(false);\n }\n },\n [client, poll],\n );\n return { poll, loading: r.loading || (!r.loaded && o.initial === undefined), error: r.error, reload: r.reload, pending, vote, answer };\n}\n\nexport interface PollFilter {\n language?: string;\n /** YYYY-MM */\n month?: string;\n /** YYYY-MM-DD */\n date?: string;\n /** Staff: every poll, scheduled and inactive ones included (PollWrite). */\n admin?: boolean;\n pageSize?: number;\n}\n\n/** Polls, newest first, a page at a time. */\nexport function usePolls(f: PollFilter = {}, o: { client?: ContentKitClient } = {}): UseList<Poll> {\n const { client, store, scope } = useContentScope(o.client);\n const size = f.pageSize ?? 20;\n const q = { language: f.language, month: f.month, date: f.date };\n return useList(\n store,\n keyOf(\"polls\", scope, !!f.admin, q.language, q.month, q.date, size),\n { type: \"polls\" },\n offsetPages(size, (page, signal) => (f.admin ? client.polls.adminList({ ...q, ...page }, signal) : client.polls.list({ ...q, ...page }, signal))),\n );\n}\n\n/** Images for a new poll, uploaded once it exists: the question's, and each option's by index. */\nexport interface PollImages {\n question?: Blob | null;\n options?: readonly (Blob | null | undefined)[];\n}\n\nexport interface UsePollEditor {\n /** null before create (or while loading). */\n poll: Poll | null;\n loading: boolean;\n error?: ContentKitError;\n /** Writes in flight. */\n saving: boolean;\n /** Creates the poll, then uploads its images; the editor then edits it. Image failures are returned, not thrown. */\n create: (input: PollInput, images?: PollImages) => Promise<{ poll: Poll; imageErrors: ContentKitError[] }>;\n update: (patch: PollUpdate) => Promise<Poll>;\n remove: () => Promise<void>;\n addOption: (label: string, image?: Blob | null) => Promise<PollOption>;\n renameOption: (option: string, label: string) => Promise<void>;\n /** Removes an option and its votes; a poll keeps at least two. */\n removeOption: (option: string) => Promise<void>;\n /** Moves an option to index to, at once; refetched if a write fails. */\n moveOption: (option: string, to: number) => Promise<void>;\n setImage: (file: Blob | null) => Promise<void>;\n setOptionImage: (option: string, file: Blob | null) => Promise<void>;\n}\n\n/** Staff: one poll's editor (PollWrite); null creates a new poll. */\nexport function usePollEditor(id: string | null | undefined, o: { client?: ContentKitClient } = {}): UsePollEditor {\n const { client, store, scope } = useContentScope(o.client);\n const [created, setCreated] = useState<string | null>(null);\n const pid = id ?? created;\n const key = pid ? keyOf(\"poll\", scope, pid) : null;\n const r = useResource<Poll | null>(store, key, { type: \"poll\", id: pid ?? undefined }, (s) => client.polls.get(pid!, s));\n const poll = r.data ?? null;\n const [inflight, setInflight] = useState(0);\n const run = useCallback(async <T>(fn: () => Promise<T>): Promise<T> => {\n setInflight((n) => n + 1);\n try {\n return await fn();\n } finally {\n setInflight((n) => n - 1);\n }\n }, []);\n const need = useCallback(() => {\n if (!pid) throw new Error(\"contentkit-ui: create the poll first\");\n return pid;\n }, [pid]);\n\n const create = useCallback<UsePollEditor[\"create\"]>(\n (input, images = {}) =>\n run(async () => {\n const made = await client.polls.create(input);\n const imageErrors: ContentKitError[] = [];\n const tries: Promise<unknown>[] = [];\n if (images.question) tries.push(client.polls.uploadImage(made.id, images.question));\n const byPosition = [...made.options].sort((a, b) => a.position - b.position);\n (input.options ?? []).forEach((opt, i) => {\n const file = images.options?.[i];\n const target = byPosition.find((x) => x.position === opt.position) ?? byPosition[i];\n if (file && target) tries.push(client.polls.uploadOptionImage(made.id, target.id, file));\n });\n for (const res of await Promise.allSettled(tries)) if (res.status === \"rejected\") imageErrors.push(toContentKitError(res.reason));\n setCreated(made.id);\n return { poll: tries.length ? await client.polls.get(made.id) : made, imageErrors };\n }),\n [client, run],\n );\n const update = useCallback((patch: PollUpdate) => run(() => client.polls.update(need(), patch)), [client, run, need]);\n const remove = useCallback(() => run(() => client.polls.delete(need())), [client, run, need]);\n const addOption = useCallback(\n (label: string, image?: Blob | null) =>\n run(async () => {\n const opt = await client.polls.addOption(need(), { label });\n if (image) opt.image_url = (await client.polls.uploadOptionImage(need(), opt.id, image)) ?? undefined;\n return opt;\n }),\n [client, run, need],\n );\n const renameOption = useCallback((option: string, label: string) => run(async () => void (await client.polls.updateOption(need(), option, { label }))), [client, run, need]);\n const removeOption = useCallback((option: string) => run(() => client.polls.deleteOption(need(), option)), [client, run, need]);\n const moveOption = useCallback(\n (option: string, to: number) =>\n run(async () => {\n const current = store.snapshot<Poll | null>(key!).data;\n if (!current) return;\n const order = [...current.options].sort((a, b) => a.position - b.position);\n const from = order.findIndex((x) => x.id === option);\n if (from < 0 || to < 0 || to >= order.length || from === to) return;\n order.splice(to, 0, ...order.splice(from, 1));\n store.set<Poll | null>(key!, { ...current, options: order.map((x, position) => ({ ...x, position })) });\n try {\n await client.polls.reorderOptions(current.id, order);\n } catch (e) {\n store.load(key!, true);\n throw e;\n }\n }),\n [client, store, key, run],\n );\n const setImage = useCallback((file: Blob | null) => run(async () => void (await client.polls.uploadImage(need(), file))), [client, run, need]);\n const setOptionImage = useCallback(\n (option: string, file: Blob | null) => run(async () => void (await client.polls.uploadOptionImage(need(), option, file))),\n [client, run, need],\n );\n return {\n poll,\n loading: !!key && (r.loading || !r.loaded),\n error: r.error,\n saving: inflight > 0,\n create,\n update,\n remove,\n addOption,\n renameOption,\n removeOption,\n moveOption,\n setImage,\n setOptionImage,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"polls.js","names":[],"sources":["../../src/react/polls.ts"],"sourcesContent":["import { useCallback, useState } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport { toContentKitError, type ContentKitError } from \"../client/errors.js\";\nimport type { Poll, PollInput, PollOption, PollUpdate } from \"../client/generated/wire.js\";\nimport { keyOf, offsetPages, useContentScope, useList, useResource, type UseList } from \"./use-resource.js\";\n\n/** The poll after the caller votes for option; a vote is final. */\nexport function withVote(p: Poll, option: string): Poll {\n if (p.voted) return p;\n return { ...p, voted: true, my_option: option, total_votes: p.total_votes + 1, options: p.options.map((o) => (o.id === option ? { ...o, vote_count: o.vote_count + 1 } : o)) };\n}\n\nexport interface UsePoll {\n /** null: no such poll (or no live one). */\n poll: Poll | null;\n loading: boolean;\n error?: ContentKitError;\n reload: () => void;\n /** A vote or answer is in flight. */\n pending: boolean;\n /** Votes at once (a vote is final); rolled back if the server refuses it. */\n vote: (option: string) => Promise<void>;\n /** Stores or replaces the signed-in caller's free-text answer. */\n answer: (text: string) => Promise<void>;\n}\n\n/**\n * A poll with the caller's vote or answer. Without an id it is the newest\n * live poll (in language): the one a home page shows.\n */\nexport function usePoll(id?: string | null, o: { initial?: Poll; language?: string; client?: ContentKitClient } = {}): UsePoll {\n const { client, store, scope } = useContentScope(o.client);\n const key = id ? keyOf(\"poll\", scope, id) : keyOf(\"poll\", scope, \"latest\", o.language ?? \"\");\n const r = useResource<Poll | null>(\n store,\n key,\n { type: \"poll\", id: id ?? undefined },\n async (s) => (id ? client.polls.get(id, s) : ((await client.polls.list({ language: o.language, limit: 1 }, s))[0] ?? null)),\n { initial: o.initial },\n );\n const [pending, setPending] = useState(false);\n const poll = r.data ?? null;\n const vote = useCallback(\n async (option: string) => {\n const prev = store.snapshot<Poll | null>(key).data ?? poll;\n if (!prev) return;\n const token = store.mark(key);\n store.set(key, withVote(prev, option));\n setPending(true);\n try {\n await client.polls.vote(prev.id, option);\n } catch (e) {\n store.rollback(key, token, prev);\n throw toContentKitError(e);\n } finally {\n setPending(false);\n }\n },\n [client, store, key, poll],\n );\n const answer = useCallback(\n async (text: string) => {\n if (!poll) return;\n setPending(true);\n try {\n await client.polls.answer(poll.id, text);\n } finally {\n setPending(false);\n }\n },\n [client, poll],\n );\n return { poll, loading: r.loading || (!r.loaded && o.initial === undefined), error: r.error, reload: r.reload, pending, vote, answer };\n}\n\nexport interface PollFilter {\n language?: string;\n /** YYYY-MM */\n month?: string;\n /** YYYY-MM-DD */\n date?: string;\n /** Staff: every poll, scheduled and inactive ones included (root:polls:write). */\n admin?: boolean;\n pageSize?: number;\n}\n\n/** Polls, newest first, a page at a time. */\nexport function usePolls(f: PollFilter = {}, o: { client?: ContentKitClient } = {}): UseList<Poll> {\n const { client, store, scope } = useContentScope(o.client);\n const size = f.pageSize ?? 20;\n const q = { language: f.language, month: f.month, date: f.date };\n return useList(\n store,\n keyOf(\"polls\", scope, !!f.admin, q.language, q.month, q.date, size),\n { type: \"polls\" },\n offsetPages(size, (page, signal) => (f.admin ? client.polls.adminList({ ...q, ...page }, signal) : client.polls.list({ ...q, ...page }, signal))),\n );\n}\n\n/** Images for a new poll, uploaded once it exists: the question's, and each option's by index. */\nexport interface PollImages {\n question?: Blob | null;\n options?: readonly (Blob | null | undefined)[];\n}\n\nexport interface UsePollEditor {\n /** null before create (or while loading). */\n poll: Poll | null;\n loading: boolean;\n error?: ContentKitError;\n /** Writes in flight. */\n saving: boolean;\n /** Creates the poll, then uploads its images; the editor then edits it. Image failures are returned, not thrown. */\n create: (input: PollInput, images?: PollImages) => Promise<{ poll: Poll; imageErrors: ContentKitError[] }>;\n update: (patch: PollUpdate) => Promise<Poll>;\n remove: () => Promise<void>;\n addOption: (label: string, image?: Blob | null) => Promise<PollOption>;\n renameOption: (option: string, label: string) => Promise<void>;\n /** Removes an option and its votes; a poll keeps at least two. */\n removeOption: (option: string) => Promise<void>;\n /** Moves an option to index to, at once; refetched if a write fails. */\n moveOption: (option: string, to: number) => Promise<void>;\n setImage: (file: Blob | null) => Promise<void>;\n setOptionImage: (option: string, file: Blob | null) => Promise<void>;\n}\n\n/** Staff: one poll's editor (root:polls:write); null creates a new poll. */\nexport function usePollEditor(id: string | null | undefined, o: { client?: ContentKitClient } = {}): UsePollEditor {\n const { client, store, scope } = useContentScope(o.client);\n const [created, setCreated] = useState<string | null>(null);\n const pid = id ?? created;\n const key = pid ? keyOf(\"poll\", scope, pid) : null;\n const r = useResource<Poll | null>(store, key, { type: \"poll\", id: pid ?? undefined }, (s) => client.polls.get(pid!, s));\n const poll = r.data ?? null;\n const [inflight, setInflight] = useState(0);\n const run = useCallback(async <T>(fn: () => Promise<T>): Promise<T> => {\n setInflight((n) => n + 1);\n try {\n return await fn();\n } finally {\n setInflight((n) => n - 1);\n }\n }, []);\n const need = useCallback(() => {\n if (!pid) throw new Error(\"contentkit-ui: create the poll first\");\n return pid;\n }, [pid]);\n\n const create = useCallback<UsePollEditor[\"create\"]>(\n (input, images = {}) =>\n run(async () => {\n const made = await client.polls.create(input);\n const imageErrors: ContentKitError[] = [];\n const tries: Promise<unknown>[] = [];\n if (images.question) tries.push(client.polls.uploadImage(made.id, images.question));\n const byPosition = [...made.options].sort((a, b) => a.position - b.position);\n (input.options ?? []).forEach((opt, i) => {\n const file = images.options?.[i];\n const target = byPosition.find((x) => x.position === opt.position) ?? byPosition[i];\n if (file && target) tries.push(client.polls.uploadOptionImage(made.id, target.id, file));\n });\n for (const res of await Promise.allSettled(tries)) if (res.status === \"rejected\") imageErrors.push(toContentKitError(res.reason));\n setCreated(made.id);\n return { poll: tries.length ? await client.polls.get(made.id) : made, imageErrors };\n }),\n [client, run],\n );\n const update = useCallback((patch: PollUpdate) => run(() => client.polls.update(need(), patch)), [client, run, need]);\n const remove = useCallback(() => run(() => client.polls.delete(need())), [client, run, need]);\n const addOption = useCallback(\n (label: string, image?: Blob | null) =>\n run(async () => {\n const opt = await client.polls.addOption(need(), { label });\n if (image) opt.image_url = (await client.polls.uploadOptionImage(need(), opt.id, image)) ?? undefined;\n return opt;\n }),\n [client, run, need],\n );\n const renameOption = useCallback((option: string, label: string) => run(async () => void (await client.polls.updateOption(need(), option, { label }))), [client, run, need]);\n const removeOption = useCallback((option: string) => run(() => client.polls.deleteOption(need(), option)), [client, run, need]);\n const moveOption = useCallback(\n (option: string, to: number) =>\n run(async () => {\n const current = store.snapshot<Poll | null>(key!).data;\n if (!current) return;\n const order = [...current.options].sort((a, b) => a.position - b.position);\n const from = order.findIndex((x) => x.id === option);\n if (from < 0 || to < 0 || to >= order.length || from === to) return;\n order.splice(to, 0, ...order.splice(from, 1));\n store.set<Poll | null>(key!, { ...current, options: order.map((x, position) => ({ ...x, position })) });\n try {\n await client.polls.reorderOptions(current.id, order);\n } catch (e) {\n store.load(key!, true);\n throw e;\n }\n }),\n [client, store, key, run],\n );\n const setImage = useCallback((file: Blob | null) => run(async () => void (await client.polls.uploadImage(need(), file))), [client, run, need]);\n const setOptionImage = useCallback(\n (option: string, file: Blob | null) => run(async () => void (await client.polls.uploadOptionImage(need(), option, file))),\n [client, run, need],\n );\n return {\n poll,\n loading: !!key && (r.loading || !r.loaded),\n error: r.error,\n saving: inflight > 0,\n create,\n update,\n remove,\n addOption,\n renameOption,\n removeOption,\n moveOption,\n setImage,\n setOptionImage,\n };\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { UseList } from './use-resource.js';
6
6
  export interface PostFilter {
7
7
  language?: string;
8
8
  sort?: Sort;
9
- /** Staff: every post, drafts and scheduled ones included (PostWrite). */
9
+ /** Staff: every post, drafts and scheduled ones included (root:posts:write). */
10
10
  admin?: boolean;
11
11
  /** With admin: true only drafts, false no drafts. */
12
12
  draft?: boolean;
@@ -29,30 +29,32 @@ export interface UsePost {
29
29
  saving: boolean;
30
30
  /** Creates the post; the hook then edits it. */
31
31
  create: (input: PostInput) => Promise<Post>;
32
- /** Updates the given fields; a body from the editor is stored with its images' public URLs (storedBody). */
32
+ /** Updates the given fields; a body from the editor is stored with image references (storedBody). */
33
33
  update: (patch: PostInput) => Promise<Post>;
34
34
  remove: () => Promise<void>;
35
- /** Restores the deleted post (PostWrite). */
35
+ /** Restores the deleted post (root:posts:write). */
36
36
  restore: () => Promise<Post>;
37
37
  /** Uploads the cover (null clears it); resolves with a URL that shows it now. */
38
38
  setCover: (file: Blob | null) => Promise<string | null>;
39
39
  /** Uploads an image for the body; resolves with a URL that shows it now, to place in the editor. */
40
40
  uploadImage: (file: Blob) => Promise<string>;
41
41
  /**
42
- * A URL that shows one of the post's images now (the cover, a body image):
43
- * an unpublished post's images are not public yet, so its editors see them
44
- * through signed URLs of an editor read; a fresh upload shows from the
45
- * file itself. Any other URL is returned as is.
42
+ * A URL that shows one of the post's images now, by name ("i-{uuid}") or
43
+ * reference: a fresh upload from its file, else its public file
44
+ * (`post.images`), else, while it has none (an unpublished post's), its
45
+ * signed editor view from an editor read. Anything else is returned as is.
46
46
  */
47
- imageSrc: (url: string | null | undefined) => string | undefined;
47
+ imageSrc: (image: string | null | undefined) => string | undefined;
48
+ /** The cover through imageSrc. */
49
+ coverSrc: string | undefined;
48
50
  /** post.body with its images through imageSrc: what a rich-text editor shows. */
49
51
  editorBody: string | undefined;
50
- /** HTML from the editor with the URLs imageSrc gave back to the images' public URLs. */
52
+ /** HTML from the editor with every image URL this hook showed turned back into the image's reference. */
51
53
  storedBody: (html: string) => string;
52
54
  }
53
55
  /** Whether readers see the post, and so its images' public files exist. */
54
56
  export declare function isPublished(post: Post, now?: number): boolean;
55
- /** One post: read, and for staff (PostWrite) create, update, delete, restore, cover and body images. null creates a new post. */
57
+ /** One post: read, and for staff (root:posts:write) create, update, delete, restore, cover and body images. null creates a new post. */
56
58
  export declare function usePost(id: string | null | undefined, o?: {
57
59
  initial?: Post;
58
60
  client?: ContentKitClient;