@jimmy_harika/websitekit 0.3.1
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.
- package/README.md +86 -0
- package/package.json +54 -0
- package/src/blocks/author-bio.tsx +52 -0
- package/src/blocks/author-hero.tsx +99 -0
- package/src/blocks/book-grid.tsx +97 -0
- package/src/blocks/book-spotlight.tsx +109 -0
- package/src/blocks/contact.tsx +64 -0
- package/src/blocks/faq.tsx +65 -0
- package/src/blocks/form.tsx +234 -0
- package/src/blocks/gallery.tsx +230 -0
- package/src/blocks/index.ts +32 -0
- package/src/blocks/newsletter.tsx +100 -0
- package/src/blocks/pricing.tsx +120 -0
- package/src/blocks/primitives.tsx +109 -0
- package/src/blocks/testimonials.tsx +89 -0
- package/src/catalogs/author/catalog.tsx +496 -0
- package/src/catalogs/author/index.ts +9 -0
- package/src/catalogs/author/templates.ts +133 -0
- package/src/catalogs/publisher/catalog.tsx +308 -0
- package/src/catalogs/publisher/index.ts +10 -0
- package/src/catalogs/publisher/templates.ts +158 -0
- package/src/editable.tsx +76 -0
- package/src/editor/BlockLibrary.tsx +110 -0
- package/src/editor/Canvas.tsx +128 -0
- package/src/editor/ErrorBoundary.tsx +54 -0
- package/src/editor/InsertPoint.tsx +31 -0
- package/src/editor/Inspector.tsx +341 -0
- package/src/editor/PageBuilder.tsx +217 -0
- package/src/editor/SectionFrame.tsx +127 -0
- package/src/editor/SectionsPanel.tsx +149 -0
- package/src/editor/TemplateGallery.tsx +98 -0
- package/src/editor/ThemePanel.tsx +131 -0
- package/src/editor/Toolbar.tsx +162 -0
- package/src/editor/dnd.tsx +61 -0
- package/src/editor/edit-primitives.tsx +164 -0
- package/src/editor/index.tsx +8 -0
- package/src/editor/shortcuts.ts +106 -0
- package/src/editor/ui-context.tsx +32 -0
- package/src/index.ts +11 -0
- package/src/lib/cn.ts +8 -0
- package/src/model.ts +116 -0
- package/src/registry.ts +138 -0
- package/src/renderer/index.tsx +55 -0
- package/src/store.ts +216 -0
- package/src/theme.ts +85 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author catalog for `@jimmy_harika/websitekit`.
|
|
3
|
+
*
|
|
4
|
+
* Composes the shared, design-system-agnostic blocks (`../../blocks`) into engine
|
|
5
|
+
* `SectionDefinition`s for author / writer websites. Lives in the package (not in
|
|
6
|
+
* any one app) so every app that serves authors inherits it — and improvements
|
|
7
|
+
* here upgrade all of them.
|
|
8
|
+
*
|
|
9
|
+
* Live data (an author's real books) is NOT fetched here: book blocks declare
|
|
10
|
+
* `items`/`item` in their schema and carry a `source: "live"` descriptor; the
|
|
11
|
+
* consuming app resolves the live list into those props before render and strips
|
|
12
|
+
* them before save (so the site stays live). See the app-side book resolver.
|
|
13
|
+
*/
|
|
14
|
+
import { type ComponentType } from "react";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import {
|
|
17
|
+
buildCatalog,
|
|
18
|
+
type Catalog,
|
|
19
|
+
type FieldDescriptor,
|
|
20
|
+
type SectionDefinition,
|
|
21
|
+
type SectionVariant,
|
|
22
|
+
} from "../../registry";
|
|
23
|
+
import {
|
|
24
|
+
AuthorBio,
|
|
25
|
+
AuthorHero,
|
|
26
|
+
BookGrid,
|
|
27
|
+
BookSpotlight,
|
|
28
|
+
Contact,
|
|
29
|
+
Faq,
|
|
30
|
+
Form,
|
|
31
|
+
Gallery,
|
|
32
|
+
Newsletter,
|
|
33
|
+
Pricing,
|
|
34
|
+
Testimonials,
|
|
35
|
+
} from "../../blocks";
|
|
36
|
+
|
|
37
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
38
|
+
/** Erase a strongly-typed block component to the engine's prop-agnostic slot.
|
|
39
|
+
* (The engine injects `edit` + validated props at runtime.) */
|
|
40
|
+
const comp = (C: ComponentType<never>): ComponentType<Record<string, unknown>> =>
|
|
41
|
+
C as unknown as ComponentType<Record<string, unknown>>;
|
|
42
|
+
|
|
43
|
+
const text = (key: string, label: string, placeholder?: string): FieldDescriptor => ({
|
|
44
|
+
key,
|
|
45
|
+
label,
|
|
46
|
+
widget: "text",
|
|
47
|
+
...(placeholder ? { placeholder } : {}),
|
|
48
|
+
});
|
|
49
|
+
const textarea = (key: string, label: string): FieldDescriptor => ({ key, label, widget: "textarea" });
|
|
50
|
+
const image = (key: string, label: string): FieldDescriptor => ({ key, label, widget: "image" });
|
|
51
|
+
const select = (
|
|
52
|
+
key: string,
|
|
53
|
+
label: string,
|
|
54
|
+
options: { value: string; label: string }[],
|
|
55
|
+
): FieldDescriptor => ({ key, label, widget: "select", options });
|
|
56
|
+
const list = (key: string, label: string, help: string): FieldDescriptor => ({
|
|
57
|
+
key,
|
|
58
|
+
label,
|
|
59
|
+
widget: "list",
|
|
60
|
+
help,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const bookItemSchema = z.object({
|
|
64
|
+
id: z.string().optional(),
|
|
65
|
+
title: z.string(),
|
|
66
|
+
subtitle: z.string().optional(),
|
|
67
|
+
coverUrl: z.string().optional(),
|
|
68
|
+
description: z.string().optional(),
|
|
69
|
+
links: z.array(z.object({ label: z.string(), url: z.string() })).optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** Parse a "Label | https://url" list-field string into a links array. */
|
|
73
|
+
function parseLinks(raw: unknown): { label: string; url: string }[] | undefined {
|
|
74
|
+
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
75
|
+
const links = raw
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((line) => line.split("|").map((s) => s.trim()))
|
|
78
|
+
.filter((parts) => parts[0])
|
|
79
|
+
.map(([label, u]) => ({ label: label ?? "", url: u ?? "#" }));
|
|
80
|
+
return links.length ? links : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
84
|
+
1. HEADER — authorHero
|
|
85
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
86
|
+
const authorHero: SectionDefinition = {
|
|
87
|
+
type: "authorHero",
|
|
88
|
+
label: "Author Hero",
|
|
89
|
+
category: "Header",
|
|
90
|
+
description: "Your name + tagline, over a portrait or as a tall type-only banner.",
|
|
91
|
+
schema: z.object({
|
|
92
|
+
eyebrow: z.string().optional(),
|
|
93
|
+
name: z.string(),
|
|
94
|
+
tagline: z.string().optional(),
|
|
95
|
+
portrait: z.string().optional(),
|
|
96
|
+
align: z.enum(["center", "left"]).optional(),
|
|
97
|
+
}),
|
|
98
|
+
defaults: {
|
|
99
|
+
eyebrow: "Author · Storyteller",
|
|
100
|
+
name: "Your Name",
|
|
101
|
+
tagline: "Novelist of quiet places and the people who leave them.",
|
|
102
|
+
portrait: "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?w=1600&q=80",
|
|
103
|
+
align: "center",
|
|
104
|
+
},
|
|
105
|
+
inline: { name: "text", tagline: "text", portrait: "image" },
|
|
106
|
+
variants: [
|
|
107
|
+
{ id: "portrait", label: "Over portrait", props: { align: "center" } },
|
|
108
|
+
{ id: "type-only", label: "Type only", props: { align: "center", portrait: "" } },
|
|
109
|
+
{ id: "split", label: "Split", props: { align: "left" } },
|
|
110
|
+
] satisfies SectionVariant[],
|
|
111
|
+
fields: [
|
|
112
|
+
text("eyebrow", "Eyebrow", "Small line above your name"),
|
|
113
|
+
text("name", "Name"),
|
|
114
|
+
text("tagline", "Tagline"),
|
|
115
|
+
image("portrait", "Portrait"),
|
|
116
|
+
select("align", "Layout", [
|
|
117
|
+
{ value: "center", label: "Centered over photo" },
|
|
118
|
+
{ value: "left", label: "Split (text + photo)" },
|
|
119
|
+
]),
|
|
120
|
+
],
|
|
121
|
+
Component: comp(AuthorHero),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
125
|
+
2. STORY — authorBio
|
|
126
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
127
|
+
const authorBio: SectionDefinition = {
|
|
128
|
+
type: "authorBio",
|
|
129
|
+
label: "About",
|
|
130
|
+
category: "Story",
|
|
131
|
+
description: "A portrait beside your story.",
|
|
132
|
+
schema: z.object({
|
|
133
|
+
eyebrow: z.string().optional(),
|
|
134
|
+
heading: z.string(),
|
|
135
|
+
body: z.string(),
|
|
136
|
+
portrait: z.string().optional(),
|
|
137
|
+
flip: z.boolean().optional(),
|
|
138
|
+
}),
|
|
139
|
+
defaults: {
|
|
140
|
+
eyebrow: "About",
|
|
141
|
+
heading: "Hello, I'm a writer.",
|
|
142
|
+
body: "I write about the places people come from and the ones they run to. My work has appeared in places I'm proud of, and my books have found readers I'll never meet.\n\nThis is the part where you tell them who you really are.",
|
|
143
|
+
portrait: "https://images.unsplash.com/photo-1499952127939-9bbf5af6c51c?w=1400&q=80",
|
|
144
|
+
flip: false,
|
|
145
|
+
},
|
|
146
|
+
inline: { heading: "text", body: "text", portrait: "image" },
|
|
147
|
+
variants: [
|
|
148
|
+
{ id: "image-left", label: "Image left", props: { flip: false } },
|
|
149
|
+
{ id: "image-right", label: "Image right", props: { flip: true } },
|
|
150
|
+
] satisfies SectionVariant[],
|
|
151
|
+
fields: [
|
|
152
|
+
text("eyebrow", "Eyebrow"),
|
|
153
|
+
text("heading", "Heading"),
|
|
154
|
+
textarea("body", "Body"),
|
|
155
|
+
image("portrait", "Portrait"),
|
|
156
|
+
],
|
|
157
|
+
Component: comp(AuthorBio),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
3. BOOKS — bookSpotlight, bookGrid (items resolved live by the app)
|
|
162
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
163
|
+
const bookSpotlight: SectionDefinition = {
|
|
164
|
+
type: "bookSpotlight",
|
|
165
|
+
label: "Book Spotlight",
|
|
166
|
+
category: "Books",
|
|
167
|
+
description: "Feature one book — cover, blurb, and buy links. Pulls your latest by default.",
|
|
168
|
+
schema: z.object({
|
|
169
|
+
eyebrow: z.string().optional(),
|
|
170
|
+
heading: z.string().optional(),
|
|
171
|
+
layout: z.enum(["cover-left", "cover-right"]).optional(),
|
|
172
|
+
source: z.enum(["live", "manual"]).optional(),
|
|
173
|
+
item: bookItemSchema.optional(),
|
|
174
|
+
}),
|
|
175
|
+
defaults: {
|
|
176
|
+
eyebrow: "New release",
|
|
177
|
+
heading: "The latest",
|
|
178
|
+
layout: "cover-left",
|
|
179
|
+
source: "live",
|
|
180
|
+
},
|
|
181
|
+
inline: { heading: "text" },
|
|
182
|
+
variants: [
|
|
183
|
+
{ id: "cover-left", label: "Cover left", props: { layout: "cover-left" } },
|
|
184
|
+
{ id: "cover-right", label: "Cover right", props: { layout: "cover-right" } },
|
|
185
|
+
] satisfies SectionVariant[],
|
|
186
|
+
fields: [
|
|
187
|
+
text("eyebrow", "Eyebrow"),
|
|
188
|
+
text("heading", "Heading"),
|
|
189
|
+
select("layout", "Cover side", [
|
|
190
|
+
{ value: "cover-left", label: "Left" },
|
|
191
|
+
{ value: "cover-right", label: "Right" },
|
|
192
|
+
]),
|
|
193
|
+
],
|
|
194
|
+
Component: comp(BookSpotlight),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const bookGrid: SectionDefinition = {
|
|
198
|
+
type: "bookGrid",
|
|
199
|
+
label: "Book Grid",
|
|
200
|
+
category: "Books",
|
|
201
|
+
description: "Your shelf — a responsive grid of your books.",
|
|
202
|
+
schema: z.object({
|
|
203
|
+
eyebrow: z.string().optional(),
|
|
204
|
+
heading: z.string().optional(),
|
|
205
|
+
columns: z.union([z.number(), z.string()]).optional(),
|
|
206
|
+
limit: z.number().optional(),
|
|
207
|
+
source: z.enum(["live", "manual"]).optional(),
|
|
208
|
+
items: z.array(bookItemSchema).optional(),
|
|
209
|
+
}),
|
|
210
|
+
defaults: {
|
|
211
|
+
eyebrow: "The shelf",
|
|
212
|
+
heading: "Books",
|
|
213
|
+
columns: 3,
|
|
214
|
+
limit: 12,
|
|
215
|
+
source: "live",
|
|
216
|
+
},
|
|
217
|
+
inline: { heading: "text" },
|
|
218
|
+
fields: [
|
|
219
|
+
text("eyebrow", "Eyebrow"),
|
|
220
|
+
text("heading", "Heading"),
|
|
221
|
+
select("columns", "Columns", [
|
|
222
|
+
{ value: "2", label: "2" },
|
|
223
|
+
{ value: "3", label: "3" },
|
|
224
|
+
{ value: "4", label: "4" },
|
|
225
|
+
]),
|
|
226
|
+
],
|
|
227
|
+
Component: comp(BookGrid),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
231
|
+
4. CONNECT — newsletter, contact
|
|
232
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
233
|
+
const newsletter: SectionDefinition = {
|
|
234
|
+
type: "newsletter",
|
|
235
|
+
label: "Newsletter",
|
|
236
|
+
category: "Connect",
|
|
237
|
+
description: "Email capture for new-release announcements.",
|
|
238
|
+
schema: z.object({
|
|
239
|
+
eyebrow: z.string().optional(),
|
|
240
|
+
heading: z.string().optional(),
|
|
241
|
+
body: z.string().optional(),
|
|
242
|
+
cta: z.string().optional(),
|
|
243
|
+
placeholder: z.string().optional(),
|
|
244
|
+
actionUrl: z.string().optional(),
|
|
245
|
+
}),
|
|
246
|
+
defaults: {
|
|
247
|
+
eyebrow: "Newsletter",
|
|
248
|
+
heading: "Stay in the loop",
|
|
249
|
+
body: "New releases, reading notes, the occasional behind-the-scenes. No spam.",
|
|
250
|
+
cta: "Subscribe",
|
|
251
|
+
},
|
|
252
|
+
fields: [
|
|
253
|
+
text("eyebrow", "Eyebrow"),
|
|
254
|
+
text("heading", "Heading"),
|
|
255
|
+
textarea("body", "Body"),
|
|
256
|
+
text("cta", "Button label"),
|
|
257
|
+
],
|
|
258
|
+
Component: comp(Newsletter),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/** Contact wraps the block to parse the inspector `socials` list-string into a
|
|
262
|
+
* `links` array (the list widget stores a raw string). */
|
|
263
|
+
const ContactWrapped: ComponentType<Record<string, unknown>> = (props) => {
|
|
264
|
+
const links = parseLinks(props.socials) ?? (props.links as { label: string; url: string }[] | undefined);
|
|
265
|
+
return <Contact {...(props as Record<string, never>)} links={links} />;
|
|
266
|
+
};
|
|
267
|
+
ContactWrapped.displayName = "ContactWrapped";
|
|
268
|
+
|
|
269
|
+
const contact: SectionDefinition = {
|
|
270
|
+
type: "contact",
|
|
271
|
+
label: "Contact",
|
|
272
|
+
category: "Connect",
|
|
273
|
+
description: "Email + social links to close the page.",
|
|
274
|
+
schema: z.object({
|
|
275
|
+
eyebrow: z.string().optional(),
|
|
276
|
+
heading: z.string().optional(),
|
|
277
|
+
body: z.string().optional(),
|
|
278
|
+
email: z.string().optional(),
|
|
279
|
+
socials: z.string().optional(),
|
|
280
|
+
links: z.array(z.object({ label: z.string(), url: z.string() })).optional(),
|
|
281
|
+
}),
|
|
282
|
+
defaults: {
|
|
283
|
+
eyebrow: "Contact",
|
|
284
|
+
heading: "Get in touch",
|
|
285
|
+
body: "For rights, events, or just to say hello.",
|
|
286
|
+
email: "hello@yoursite.com",
|
|
287
|
+
socials: "Instagram | https://instagram.com\nGoodreads | https://goodreads.com",
|
|
288
|
+
},
|
|
289
|
+
inline: { heading: "text", body: "text" },
|
|
290
|
+
fields: [
|
|
291
|
+
text("eyebrow", "Eyebrow"),
|
|
292
|
+
text("heading", "Heading"),
|
|
293
|
+
textarea("body", "Body"),
|
|
294
|
+
text("email", "Email"),
|
|
295
|
+
list("socials", "Social links", "One per line: Label | https://url"),
|
|
296
|
+
],
|
|
297
|
+
Component: ContactWrapped,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
301
|
+
7. GALLERY — gallery (portfolio / photo grid + lightbox)
|
|
302
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
303
|
+
const gallery: SectionDefinition = {
|
|
304
|
+
type: "gallery",
|
|
305
|
+
label: "Gallery",
|
|
306
|
+
category: "Images",
|
|
307
|
+
description: "Photo grid or masonry with a lightbox.",
|
|
308
|
+
schema: z.object({
|
|
309
|
+
heading: z.string().optional(),
|
|
310
|
+
images: z.string().optional(),
|
|
311
|
+
columns: z.enum(["2", "3", "4"]).optional(),
|
|
312
|
+
layout: z.enum(["grid", "masonry"]).optional(),
|
|
313
|
+
aspect: z.enum(["auto", "square", "portrait", "landscape"]).optional(),
|
|
314
|
+
}),
|
|
315
|
+
defaults: {
|
|
316
|
+
heading: "Recent work",
|
|
317
|
+
images:
|
|
318
|
+
"https://images.unsplash.com/photo-1519225421980-715cb0215aed?w=1200&q=80 | Wedding ceremony\nhttps://images.unsplash.com/photo-1606216794074-735e91aa2c92?w=1200&q=80 | Portrait session\nhttps://images.unsplash.com/photo-1519741497674-611481863552?w=1200&q=80 | Reception\nhttps://images.unsplash.com/photo-1511285560929-80b456fea0bc?w=1200&q=80 | First dance\nhttps://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?w=1200&q=80 | Details\nhttps://images.unsplash.com/photo-1519225421980-715cb0215aed?w=1200&q=80 | Vows",
|
|
319
|
+
columns: "3",
|
|
320
|
+
layout: "grid",
|
|
321
|
+
aspect: "portrait",
|
|
322
|
+
},
|
|
323
|
+
inline: { heading: "text" },
|
|
324
|
+
variants: [
|
|
325
|
+
{ id: "grid-3", label: "Grid · 3", props: { layout: "grid", columns: "3" } },
|
|
326
|
+
{ id: "grid-2", label: "Grid · 2", props: { layout: "grid", columns: "2" } },
|
|
327
|
+
{ id: "masonry", label: "Masonry", props: { layout: "masonry" } },
|
|
328
|
+
] satisfies SectionVariant[],
|
|
329
|
+
fields: [
|
|
330
|
+
text("heading", "Heading"),
|
|
331
|
+
list("images", "Images", "One per line: https://image-url | optional alt text"),
|
|
332
|
+
select("layout", "Layout", [
|
|
333
|
+
{ value: "grid", label: "Grid (fixed aspect)" },
|
|
334
|
+
{ value: "masonry", label: "Masonry (natural)" },
|
|
335
|
+
]),
|
|
336
|
+
select("columns", "Columns", [
|
|
337
|
+
{ value: "2", label: "2" },
|
|
338
|
+
{ value: "3", label: "3" },
|
|
339
|
+
{ value: "4", label: "4" },
|
|
340
|
+
]),
|
|
341
|
+
select("aspect", "Aspect (grid only)", [
|
|
342
|
+
{ value: "auto", label: "Auto" },
|
|
343
|
+
{ value: "square", label: "Square" },
|
|
344
|
+
{ value: "portrait", label: "Portrait 3:4" },
|
|
345
|
+
{ value: "landscape", label: "Landscape 4:3" },
|
|
346
|
+
]),
|
|
347
|
+
],
|
|
348
|
+
Component: comp(Gallery),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
352
|
+
8. PRICING — pricing
|
|
353
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
354
|
+
const pricing: SectionDefinition = {
|
|
355
|
+
type: "pricing",
|
|
356
|
+
label: "Pricing",
|
|
357
|
+
category: "Services",
|
|
358
|
+
description: "Package cards with features + a call-to-action.",
|
|
359
|
+
schema: z.object({
|
|
360
|
+
heading: z.string().optional(),
|
|
361
|
+
plans: z.string().optional(),
|
|
362
|
+
highlight: z.string().optional(),
|
|
363
|
+
}),
|
|
364
|
+
defaults: {
|
|
365
|
+
heading: "Plans & pricing",
|
|
366
|
+
plans:
|
|
367
|
+
"Essential | $1,200 | | 2-hour coverage; online gallery; 200 edited photos | Book | /contact\nSignature | $2,400 | | 6-hour coverage; second shooter; 400 edited photos; print credit | Book | /contact\nBespoke | Custom | | Full-day coverage; album; rush delivery | Enquire | /contact",
|
|
368
|
+
highlight: "2",
|
|
369
|
+
},
|
|
370
|
+
inline: { heading: "text" },
|
|
371
|
+
fields: [
|
|
372
|
+
text("heading", "Heading"),
|
|
373
|
+
list(
|
|
374
|
+
"plans",
|
|
375
|
+
"Plans",
|
|
376
|
+
"One per line: Name | price | cadence | feature; feature; feature | ctaLabel | ctaHref",
|
|
377
|
+
),
|
|
378
|
+
select("highlight", "Highlight", [
|
|
379
|
+
{ value: "", label: "None" },
|
|
380
|
+
{ value: "1", label: "1st" },
|
|
381
|
+
{ value: "2", label: "2nd" },
|
|
382
|
+
{ value: "3", label: "3rd" },
|
|
383
|
+
]),
|
|
384
|
+
],
|
|
385
|
+
Component: comp(Pricing),
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
389
|
+
9. TESTIMONIALS — testimonials
|
|
390
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
391
|
+
const testimonials: SectionDefinition = {
|
|
392
|
+
type: "testimonials",
|
|
393
|
+
label: "Testimonials",
|
|
394
|
+
category: "Proof",
|
|
395
|
+
description: "Quotes from clients.",
|
|
396
|
+
schema: z.object({
|
|
397
|
+
heading: z.string().optional(),
|
|
398
|
+
items: z.string().optional(),
|
|
399
|
+
layout: z.enum(["single", "grid"]).optional(),
|
|
400
|
+
}),
|
|
401
|
+
defaults: {
|
|
402
|
+
heading: "Kind words",
|
|
403
|
+
layout: "grid",
|
|
404
|
+
items:
|
|
405
|
+
"Working with them was effortless — the gallery took our breath away.\nMaya & Dan | Wedding, Spring 2025\nFrom the first email to the final gallery, everything was handled. We felt completely at ease.\nPriya & Arjun | Engagement\nThe photos are stunning and the turnaround was so fast. Could not recommend more.\nThe Hendricks | Family session",
|
|
406
|
+
},
|
|
407
|
+
inline: { heading: "text" },
|
|
408
|
+
variants: [
|
|
409
|
+
{ id: "grid", label: "Grid", props: { layout: "grid" } },
|
|
410
|
+
{ id: "single", label: "Featured quote", props: { layout: "single" } },
|
|
411
|
+
] satisfies SectionVariant[],
|
|
412
|
+
fields: [
|
|
413
|
+
text("heading", "Heading"),
|
|
414
|
+
list("items", "Quotes", "One per line: quote | author | role"),
|
|
415
|
+
],
|
|
416
|
+
Component: comp(Testimonials),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
420
|
+
10. FAQ — faq
|
|
421
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
422
|
+
const faq: SectionDefinition = {
|
|
423
|
+
type: "faq",
|
|
424
|
+
label: "FAQ",
|
|
425
|
+
category: "Info",
|
|
426
|
+
description: "Accordion of common questions.",
|
|
427
|
+
schema: z.object({
|
|
428
|
+
heading: z.string().optional(),
|
|
429
|
+
items: z.string().optional(),
|
|
430
|
+
}),
|
|
431
|
+
defaults: {
|
|
432
|
+
heading: "FAQ",
|
|
433
|
+
items:
|
|
434
|
+
"How soon will we get our photos? | Galleries are delivered within 2 weeks; rush delivery available.\nDo you travel? | Yes — anywhere. Travel within 50 miles is included; beyond that at cost.\nHow do we book? | A signed contract + retainer reserves your date. Reach out via the form below.",
|
|
435
|
+
},
|
|
436
|
+
inline: { heading: "text" },
|
|
437
|
+
fields: [
|
|
438
|
+
text("heading", "Heading"),
|
|
439
|
+
list("items", "Q & A", "One per line: question | answer"),
|
|
440
|
+
],
|
|
441
|
+
Component: comp(Faq),
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
445
|
+
11. FORM — form (contact / inquiry / lead)
|
|
446
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
447
|
+
const form: SectionDefinition = {
|
|
448
|
+
type: "form",
|
|
449
|
+
label: "Form",
|
|
450
|
+
category: "Connect",
|
|
451
|
+
description: "Configurable contact / inquiry form. POSTs to an actionUrl.",
|
|
452
|
+
schema: z.object({
|
|
453
|
+
heading: z.string().optional(),
|
|
454
|
+
fields: z.string().optional(),
|
|
455
|
+
buttonLabel: z.string().optional(),
|
|
456
|
+
successMessage: z.string().optional(),
|
|
457
|
+
actionUrl: z.string().optional(),
|
|
458
|
+
turnstileSiteKey: z.string().optional(),
|
|
459
|
+
}),
|
|
460
|
+
defaults: {
|
|
461
|
+
heading: "Get in touch",
|
|
462
|
+
fields: "Your name | name | text\nEmail | email | email\nEvent date | date | date\nTell us about your day | message | textarea",
|
|
463
|
+
buttonLabel: "Send",
|
|
464
|
+
successMessage: "Thanks — we'll be in touch within 24 hours.",
|
|
465
|
+
actionUrl: "",
|
|
466
|
+
turnstileSiteKey: "",
|
|
467
|
+
},
|
|
468
|
+
inline: { heading: "text" },
|
|
469
|
+
fields: [
|
|
470
|
+
text("heading", "Heading"),
|
|
471
|
+
list("fields", "Fields", "One per line: label | name | type (text/email/tel/textarea/select/date/checkbox)"),
|
|
472
|
+
text("buttonLabel", "Button label"),
|
|
473
|
+
textarea("successMessage", "Success message"),
|
|
474
|
+
text("actionUrl", "Submit URL (app Convex action)"),
|
|
475
|
+
text("turnstileSiteKey", "Cloudflare Turnstile site key (optional)"),
|
|
476
|
+
],
|
|
477
|
+
Component: comp(Form),
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/** The author catalog — ordered to emit: Header → Story → Books → Connect. */
|
|
481
|
+
export const AUTHOR_CATALOG: Catalog = buildCatalog([
|
|
482
|
+
authorHero,
|
|
483
|
+
authorBio,
|
|
484
|
+
bookSpotlight,
|
|
485
|
+
bookGrid,
|
|
486
|
+
gallery,
|
|
487
|
+
pricing,
|
|
488
|
+
testimonials,
|
|
489
|
+
faq,
|
|
490
|
+
form,
|
|
491
|
+
newsletter,
|
|
492
|
+
contact,
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
/** Section types whose `items`/`item` the app should resolve from live books. */
|
|
496
|
+
export const AUTHOR_BOOK_SECTION_TYPES = ["bookSpotlight", "bookGrid"] as const;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@jimmy_harika/websitekit/catalogs/author` — the author-website vertical:
|
|
3
|
+
* catalog (block definitions) + starter templates. A consuming app imports
|
|
4
|
+
* `AUTHOR_CATALOG` for the editor + renderer, `AUTHOR_TEMPLATES` for the
|
|
5
|
+
* template gallery, and resolves live books into `AUTHOR_BOOK_SECTION_TYPES`.
|
|
6
|
+
*/
|
|
7
|
+
export { AUTHOR_CATALOG, AUTHOR_BOOK_SECTION_TYPES } from "./catalog";
|
|
8
|
+
export { AUTHOR_TEMPLATES, blankAuthorDocument } from "./templates";
|
|
9
|
+
export { type BookItem } from "../../blocks";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author website templates — complete starter sites on the author catalog.
|
|
3
|
+
* Each `build()` returns a themed `PageDocument`. Book sections carry only a
|
|
4
|
+
* `source: "live"` descriptor; the app injects the author's real books at render.
|
|
5
|
+
*/
|
|
6
|
+
import { createSectionId, type PageDocument, type Section, type TemplateMeta } from "../../model";
|
|
7
|
+
|
|
8
|
+
const sec = (type: string, props: Record<string, unknown>, variant?: string): Section => ({
|
|
9
|
+
id: createSectionId(),
|
|
10
|
+
type,
|
|
11
|
+
...(variant ? { variant } : {}),
|
|
12
|
+
props,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/* ── Quill — warm literary serif, light ─────────────────────────────────────── */
|
|
16
|
+
function quill(): PageDocument {
|
|
17
|
+
return {
|
|
18
|
+
theme: {
|
|
19
|
+
fontHeading: "Cormorant Garamond",
|
|
20
|
+
fontBody: "Jost",
|
|
21
|
+
colors: { bg: "#fbf9f5", text: "#23201b", primary: "#23201b", accent: "#9a7b4f", muted: "#75706a" },
|
|
22
|
+
radius: "sm",
|
|
23
|
+
spacing: "roomy",
|
|
24
|
+
},
|
|
25
|
+
sections: [
|
|
26
|
+
sec("authorHero", {
|
|
27
|
+
eyebrow: "Author · Storyteller",
|
|
28
|
+
name: "Eleanor Vance",
|
|
29
|
+
tagline: "Novelist of quiet places and the people who leave them.",
|
|
30
|
+
portrait: "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?w=1600&q=80",
|
|
31
|
+
align: "center",
|
|
32
|
+
}),
|
|
33
|
+
sec("authorBio", {
|
|
34
|
+
eyebrow: "About",
|
|
35
|
+
heading: "I write about leaving, and what's left.",
|
|
36
|
+
body: "Three novels in, I'm still chasing the same question: what do we carry when we go? My books have found readers in places I'll never visit, and that still undoes me.\n\nWhen I'm not writing I'm reading other people's, walking the same coast path, and answering letters slower than I should.",
|
|
37
|
+
portrait: "https://images.unsplash.com/photo-1499952127939-9bbf5af6c51c?w=1400&q=80",
|
|
38
|
+
flip: false,
|
|
39
|
+
}, "image-left"),
|
|
40
|
+
sec("bookSpotlight", {
|
|
41
|
+
eyebrow: "New release",
|
|
42
|
+
heading: "The latest",
|
|
43
|
+
layout: "cover-left",
|
|
44
|
+
source: "live",
|
|
45
|
+
}, "cover-left"),
|
|
46
|
+
sec("bookGrid", { eyebrow: "The shelf", heading: "Every book", columns: 3, limit: 12, source: "live" }),
|
|
47
|
+
sec("newsletter", {
|
|
48
|
+
eyebrow: "Newsletter",
|
|
49
|
+
heading: "First to know",
|
|
50
|
+
body: "New books, reading notes, the occasional letter. No spam, ever.",
|
|
51
|
+
cta: "Subscribe",
|
|
52
|
+
}),
|
|
53
|
+
sec("contact", {
|
|
54
|
+
eyebrow: "Contact",
|
|
55
|
+
heading: "Say hello",
|
|
56
|
+
body: "For rights, events, or a kind word.",
|
|
57
|
+
email: "hello@eleanorvance.com",
|
|
58
|
+
socials: "Instagram | https://instagram.com\nGoodreads | https://goodreads.com",
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── Ink — modern dark, cinematic ──────────────────────────────────────────── */
|
|
65
|
+
function ink(): PageDocument {
|
|
66
|
+
return {
|
|
67
|
+
theme: {
|
|
68
|
+
fontHeading: "Fraunces",
|
|
69
|
+
fontBody: "Manrope",
|
|
70
|
+
colors: { bg: "#0f0f10", text: "#f3f1ec", primary: "#f3f1ec", accent: "#c9a36a", muted: "#9b958c" },
|
|
71
|
+
radius: "md",
|
|
72
|
+
spacing: "normal",
|
|
73
|
+
},
|
|
74
|
+
sections: [
|
|
75
|
+
sec("authorHero", {
|
|
76
|
+
eyebrow: "Thriller · Suspense",
|
|
77
|
+
name: "Marcus Reed",
|
|
78
|
+
tagline: "He writes the kind of books you finish at 3am and regret in the morning.",
|
|
79
|
+
portrait: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=1600&q=80",
|
|
80
|
+
align: "center",
|
|
81
|
+
}, "portrait"),
|
|
82
|
+
sec("bookSpotlight", {
|
|
83
|
+
eyebrow: "Out now",
|
|
84
|
+
heading: "The new one",
|
|
85
|
+
layout: "cover-right",
|
|
86
|
+
source: "live",
|
|
87
|
+
}, "cover-right"),
|
|
88
|
+
sec("authorBio", {
|
|
89
|
+
eyebrow: "About",
|
|
90
|
+
heading: "Twelve books. One bad habit.",
|
|
91
|
+
body: "I've been called a 'master of the slow dread' by people who should know. Before this I did a lot of jobs I won't list. Now I make things up for a living and try to make them feel true.",
|
|
92
|
+
portrait: "https://images.unsplash.com/photo-1488161628813-04466f872be2?w=1400&q=80",
|
|
93
|
+
flip: true,
|
|
94
|
+
}, "image-right"),
|
|
95
|
+
sec("bookGrid", { eyebrow: "Backlist", heading: "The whole run", columns: 4, limit: 16, source: "live" }),
|
|
96
|
+
sec("newsletter", {
|
|
97
|
+
eyebrow: "The list",
|
|
98
|
+
heading: "Get the next one early",
|
|
99
|
+
body: "Cover reveals, early chapters, release dates. Straight to your inbox.",
|
|
100
|
+
cta: "Join",
|
|
101
|
+
}),
|
|
102
|
+
sec("contact", {
|
|
103
|
+
eyebrow: "Contact",
|
|
104
|
+
heading: "Get in touch",
|
|
105
|
+
body: "Film/TV rights and events via my agent; everything else, here.",
|
|
106
|
+
email: "marcus@marcusreed.com",
|
|
107
|
+
socials: "X | https://x.com\nGoodreads | https://goodreads.com",
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const AUTHOR_TEMPLATES: TemplateMeta[] = [
|
|
114
|
+
{
|
|
115
|
+
id: "quill",
|
|
116
|
+
name: "Quill",
|
|
117
|
+
category: "Author",
|
|
118
|
+
description: "Warm literary serif on cream — for novelists and essayists.",
|
|
119
|
+
build: quill,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "ink",
|
|
123
|
+
name: "Ink",
|
|
124
|
+
category: "Author",
|
|
125
|
+
description: "Modern dark and cinematic — for thriller, crime, and genre.",
|
|
126
|
+
build: ink,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
/** The blank starting point when no template is chosen. */
|
|
131
|
+
export function blankAuthorDocument(): PageDocument {
|
|
132
|
+
return quill();
|
|
133
|
+
}
|