hazo_blog 1.3.0 → 1.6.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 (89) hide show
  1. package/CHANGE_LOG.md +166 -0
  2. package/README.md +11 -0
  3. package/db_setup_postgres.sql +2 -0
  4. package/db_setup_sqlite.sql +1 -0
  5. package/dist/components/admin/post-form.d.ts +4 -2
  6. package/dist/components/admin/post-form.d.ts.map +1 -1
  7. package/dist/components/admin/post-form.js +24 -18
  8. package/dist/components/author-bio.d.ts +3 -1
  9. package/dist/components/author-bio.d.ts.map +1 -1
  10. package/dist/components/author-bio.js +3 -4
  11. package/dist/components/blog-search.d.ts +3 -1
  12. package/dist/components/blog-search.d.ts.map +1 -1
  13. package/dist/components/blog-search.js +4 -5
  14. package/dist/components/editorial-post-card.d.ts +3 -1
  15. package/dist/components/editorial-post-card.d.ts.map +1 -1
  16. package/dist/components/editorial-post-card.js +2 -2
  17. package/dist/components/faq-section.d.ts +3 -1
  18. package/dist/components/faq-section.d.ts.map +1 -1
  19. package/dist/components/faq-section.js +4 -5
  20. package/dist/components/featured-post-card.d.ts +3 -1
  21. package/dist/components/featured-post-card.d.ts.map +1 -1
  22. package/dist/components/featured-post-card.js +4 -5
  23. package/dist/components/layouts/blog-index-editorial.d.ts +1 -1
  24. package/dist/components/layouts/blog-index-editorial.d.ts.map +1 -1
  25. package/dist/components/layouts/blog-index-editorial.js +6 -7
  26. package/dist/components/layouts/blog-index-grid.d.ts +1 -1
  27. package/dist/components/layouts/blog-index-grid.d.ts.map +1 -1
  28. package/dist/components/layouts/blog-index-grid.js +6 -7
  29. package/dist/components/layouts/index.d.ts +2 -0
  30. package/dist/components/layouts/index.d.ts.map +1 -1
  31. package/dist/components/post-card.d.ts +3 -1
  32. package/dist/components/post-card.d.ts.map +1 -1
  33. package/dist/components/post-card.js +5 -5
  34. package/dist/components/post-hero.d.ts +3 -1
  35. package/dist/components/post-hero.d.ts.map +1 -1
  36. package/dist/components/post-hero.js +5 -13
  37. package/dist/components/related-posts.d.ts +3 -1
  38. package/dist/components/related-posts.d.ts.map +1 -1
  39. package/dist/components/related-posts.js +4 -5
  40. package/dist/components/table-of-contents.d.ts +3 -1
  41. package/dist/components/table-of-contents.d.ts.map +1 -1
  42. package/dist/components/table-of-contents.js +5 -5
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/lib/dates.d.ts +6 -1
  47. package/dist/lib/dates.d.ts.map +1 -1
  48. package/dist/lib/dates.js +14 -2
  49. package/dist/lib/i18n.d.ts +28 -0
  50. package/dist/lib/i18n.d.ts.map +1 -0
  51. package/dist/lib/i18n.js +74 -0
  52. package/dist/lib/index.d.ts +1 -0
  53. package/dist/lib/index.d.ts.map +1 -1
  54. package/dist/lib/index.js +7 -0
  55. package/dist/lib/mdx_mask.d.ts +28 -0
  56. package/dist/lib/mdx_mask.d.ts.map +1 -0
  57. package/dist/lib/mdx_mask.js +165 -0
  58. package/dist/lib/mdx_segment.d.ts +15 -0
  59. package/dist/lib/mdx_segment.d.ts.map +1 -0
  60. package/dist/lib/mdx_segment.js +56 -0
  61. package/dist/locales/de.json +111 -0
  62. package/dist/locales/en.json +20 -1
  63. package/dist/locales/sv.json +111 -0
  64. package/dist/next/pages.d.ts +1 -0
  65. package/dist/next/pages.d.ts.map +1 -1
  66. package/dist/next/pages.js +24 -17
  67. package/dist/next/routes.d.ts +33 -0
  68. package/dist/next/routes.d.ts.map +1 -1
  69. package/dist/next/routes.js +115 -3
  70. package/dist/repository/index.d.ts +2 -0
  71. package/dist/repository/index.d.ts.map +1 -1
  72. package/dist/repository/index.js +7 -0
  73. package/dist/seo/index.d.ts +10 -2
  74. package/dist/seo/index.d.ts.map +1 -1
  75. package/dist/seo/index.js +17 -5
  76. package/dist/service/index.d.ts +88 -0
  77. package/dist/service/index.d.ts.map +1 -1
  78. package/dist/service/index.js +268 -0
  79. package/dist/types/index.d.ts +16 -0
  80. package/dist/types/index.d.ts.map +1 -1
  81. package/dist/ui/blog-admin-panel.d.ts.map +1 -1
  82. package/dist/ui/blog-admin-panel.js +20 -1
  83. package/dist/ui/post-list-table.d.ts +9 -1
  84. package/dist/ui/post-list-table.d.ts.map +1 -1
  85. package/dist/ui/post-list-table.js +10 -2
  86. package/dist/ui/use-blog-admin.d.ts +7 -0
  87. package/dist/ui/use-blog-admin.d.ts.map +1 -1
  88. package/dist/ui/use-blog-admin.js +24 -5
  89. package/package.json +11 -7
package/CHANGE_LOG.md CHANGED
@@ -3,6 +3,172 @@
3
3
  All notable changes are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## 1.6.0 — 2026-10-09
7
+
8
+ **Blog translation, Phase 4C: AI translate + AI review of translation-cluster drafts**
9
+
10
+ Takes a Phase 4B draft (created via `createTranslation()`, content copied verbatim,
11
+ untranslated) and fills it in — plus an independent quality-review pass. Both are new
12
+ `BlogService` methods over the `hazo_llm_api` optional peer, mirroring `hazo_i18n`'s own
13
+ `api/translate.ts`/`api/review.ts` shape and philosophy exactly (lazy `optional_import()`
14
+ probe, local re-declared module-shape interface, per-call `HazoUnavailableError` 503,
15
+ per-chunk graceful degradation).
16
+
17
+ - **`src/lib/mdx_mask.ts`** — `maskMdx()`/`unmaskMdx()`. Sets aside leading YAML frontmatter
18
+ (defensive only — `BlogPost.content` is pure MDX body today, title/excerpt/meta live in
19
+ their own columns, so no post in this schema actually has frontmatter), masks every
20
+ capitalized JSX component tag (`<Callout>`, `<YouTube id="..."/>`, ANY `[A-Z]...` tag name —
21
+ not an enumerated list, since a host's `config.mdxComponents` can add more) into its own
22
+ `jN` placeholder namespace, then delegates the remainder to `hazo_i18n/html`'s
23
+ `maskHtml()`/`unmaskHtml()` (`xN` namespace) for plain attributed HTML tags. The JSX pass
24
+ exists because `maskHtml()` only masks tags that carry attributes — right for plain HTML,
25
+ wrong for MDX, where a bare `<Callout>` (no attrs) is exactly as fragile to translate
26
+ around as an attributed one. **`hazo_i18n/html` is resolved LAZILY** (`optional_import`,
27
+ same convention as the `hazo_llm_api` peer below) — a static import here would turn
28
+ `hazo_i18n` (an optional peer) into a hard dependency for every consumer of this package,
29
+ not just ones calling `translatePost()`. For the same reason this module is NOT re-exported
30
+ through the general `./lib` barrel.
31
+ - **`src/lib/mdx_segment.ts`** — `segmentMdxForTranslation(content, maxChars = 2000)`. Splits
32
+ masked MDX at paragraph (blank-line) boundaries into LLM-call-sized chunks, grouping
33
+ consecutive paragraphs up to the budget; a single oversized paragraph still gets its own
34
+ chunk rather than being split mid-paragraph.
35
+ - **`BlogService.translatePost(postId)`** — `postId` is an EXISTING translation-cluster
36
+ DRAFT's own id (not a new `targetLocale` parameter): its `translation_of` is read to find
37
+ the source post, and its own `locale` is the target — a deliberate refinement of the
38
+ original plan now that `createTranslation()` already owns "create the row". Masks the
39
+ source content, segments it, translates each segment sequentially with its own
40
+ `hazo_llm_text_text()` call (plain-text response — "return ONLY the translated text, no
41
+ commentary" — not JSON; there's nothing to extract-as-JSON for free-form prose, unlike
42
+ `hazo_i18n`'s key-value chunks), then the title with one more call. One segment's failure
43
+ never aborts the others, but if ANY segment (or the title) fails, nothing is written — a
44
+ partially translated MDX post going live is worse than failing the whole call. Never
45
+ changes `status` (stays `'draft'`).
46
+ - **`BlogService.reviewTranslatedPost(postId)`** — one LLM call (not chunked; a blog post
47
+ comfortably fits one call) judging accuracy/naturalness/completeness/MDX-preservation
48
+ against the source, requesting structured `{ ok, notes }` JSON (parsed via
49
+ `try_extract_json()`, since a free-text verdict would need re-parsing anyway). Persists
50
+ `notes` to the post's new `review_notes` column on success; a failed review call leaves
51
+ `review_notes` untouched (`persisted: false`) rather than overwriting it with an error.
52
+ Never flips `status` — unlike `hazo_i18n`'s `reviewMessages()`, a human editor decides what
53
+ to do with the notes via the existing admin UI.
54
+ - **`hazo_blog_posts.review_notes TEXT`** (nullable) — new column, both `db_setup_sqlite.sql`
55
+ and `db_setup_postgres.sql` (+ their `migrations/` mirrors). `excerpt` was deliberately NOT
56
+ repurposed — it's an actively-used teaser/meta-description field.
57
+ - **New routes** (`src/next/routes.ts`, exported from the package root): `createBlogTranslateRoutes`
58
+ (`[id]/translate`, `POST` → `translatePost`) and `createBlogReviewRoutes` (`[id]/review`,
59
+ `POST` → `reviewTranslatedPost`) — mounted on their own resources rather than another method
60
+ on `[id]`, since that route's `POST`/`PUT`/`DELETE` are already spoken for (Phase 4B's
61
+ `createTranslation`, plus upsert/delete). Same cookie/session `config.authorize` guard, same
62
+ `HazoUnavailableError` → 503 convention as everywhere else in this file.
63
+ - **`hazo_llm_api` is now a declared OPTIONAL peer** (`peerDependenciesMeta`), same convention
64
+ as `hazo_i18n`. **`hazo_i18n`'s declared range bumped `^0.9.0` → `^0.12.0`** — its new `./html`
65
+ subpath (this phase's `maskHtml`/`unmaskHtml` reuse) only exists from 0.12.0. Note: this
66
+ workspace's own `node_modules/hazo_i18n` held a STALE real copy (0.5.9 — not even satisfying
67
+ the old `^0.9.0` range) that shadowed the correctly-hoisted 0.12.0 at the monorepo root;
68
+ removed so resolution falls through to the hoisted copy. A fresh `npm install` anywhere this
69
+ stale-copy situation doesn't already exist should resolve cleanly against the declared range.
70
+
71
+ ## 1.5.0 — 2026-10-09
72
+
73
+ **Blog translation, Phase 4B: locale-aware post URLs + static params + admin "Translate to..."**
74
+
75
+ Closes three gaps left over from 1.4.0's locale infrastructure (locale columns, `translation_of`,
76
+ locale-filtered repository reads, `getTranslations`) — URLs, static generation, and the admin UI
77
+ never actually used any of it.
78
+
79
+ - **`postUrl(config, slug, locale?)`** (`src/seo/index.ts`) gained an optional third parameter.
80
+ Omitted, `null`, or equal to `config.defaultLocale` (`"en"` when unset): byte-identical output
81
+ to before this parameter existed — no prefix, fully backward compatible. Any other locale gets
82
+ a `/${locale}` segment prepended **before** `basePath`: `https://site.com/de/blog/my-slug`, not
83
+ `/blog/de/my-slug` — matching gotimer's own `/[locale]/<slug>` convention (locale segment
84
+ first, default locale unprefixed) so a locale-aware host's `generateStaticParams`/routing
85
+ doesn't have to special-case blog URLs. `canonicalFor` now threads the post's own `locale`
86
+ through (a translated post's canonical was previously default-locale-shaped), and the sitemap's
87
+ per-post URL and hreflang `alternates` do the same.
88
+ - **`createBlogPostPage(...).generateStaticParams`** now returns a `{ slug, locale }` entry for
89
+ **every** locale variant from `getAllSlugs(false)` — not just the default locale. Null-locale
90
+ (default) rows get `config.defaultLocale` (`"en"` fallback) filled in as their `locale` value,
91
+ matching `postUrl`'s own default-locale comparison. Previously only default-locale posts ever
92
+ got a static route; every translation 404'd until `dynamicParams` kicked in on-demand.
93
+ - **Admin locale filter + "Translate to..."** (`BlogAdminPanel`, `PostListTable`,
94
+ `useBlogAdmin`, `createBlogAdminRoutes`):
95
+ - `collection.GET` now reads `?locale=` (same omitted-means-default-locale semantics as
96
+ `ListPostsOptions.locale`) and also returns a derived `locales: string[]` — every locale
97
+ currently in use, default locale first. No new DB query: reuses the existing
98
+ `getAllSlugs(true)` lookup (already built for `generateStaticParams`) rather than adding an
99
+ `enabledLocales` config field, since none exists today.
100
+ - New `BlogService.createTranslation(postId, targetLocale, req?)` + `BlogRepository.getPostById`
101
+ — creates a **draft** post in `targetLocale`, linked via `translation_of` to the source's
102
+ cluster root, with every content field (title/content/excerpt/meta/FAQ/tags/images) copied
103
+ **verbatim** from the source. Not a translation step — no LLM call; that's Phase 4C.
104
+ - New `item.POST` route (same `[id]` resource as the existing `PUT`/`DELETE`) wraps
105
+ `createTranslation`; body `{ locale: string }`.
106
+ - `BlogAdminPanel` gained a locale `<select>` filtering the post list (hidden when only one
107
+ locale is in use); `PostListTable` gained a Locale column and a per-row "Translate to…"
108
+ picker + button (hidden when `availableLocales` is empty). Both token-free Tailwind, per this
109
+ tree's isolation rule. New locale catalog keys (`post_list_table.col_locale`,
110
+ `post_list_table.translate_to`, `post_list_table.translate_failed`,
111
+ `blog_admin_panel.locale_filter_label`) in `en`/`de`/`sv`.
112
+ - Out of scope (separate, later phases): gotimer's own hazo_blog 1.4.0 schema upgrade (4A) and
113
+ the LLM `translatePost`/`reviewTranslatedPost` pipeline (4C) — `createTranslation` deliberately
114
+ does a verbatim copy, not a translation.
115
+
116
+ ## 1.4.0 — 2026-10-06
117
+
118
+ **Locale-aware UI text and dates (`en` / `de` / `sv`)**
119
+
120
+ The package's own user-visible text was English-only, and `createPackageT` cannot serve a
121
+ different locale (it bundles one English catalog and has no synchronous locale getter). This
122
+ release adds a per-render translator and a `locale` prop without changing any default output.
123
+
124
+ **New: `src/lib/i18n.ts`** — `blogTranslator(locale)` returns a `t()` bound to the matching
125
+ catalog, with English fallback for missing keys or unshipped locales. Also `resolveBlogLocale`
126
+ (`"de-AT"` → `de`, unknown → `en`), `pluralSuffix(locale, count)` (for the one plural-dependent
127
+ key), `BLOG_LOCALES`. Exported from `hazo_blog/lib`.
128
+
129
+ **New catalogs: `src/locales/de.json`, `src/locales/sv.json`** — same 105 keys as `en.json`
130
+ (enforced by a test). `en.json` grew from 90 to 105 keys: toolbar labels (`post_form.embed_*`),
131
+ status badges (`post_status.*`), and admin page chrome (`admin_pages.*`).
132
+
133
+ - **de / sv values are first-pass drafts and need translation review.** Every value is a real
134
+ translation (no English placeholders), but none has been reviewed by a native speaker. Priority
135
+ review: admin-facing strings (`post_form.*`, `blog_admin_panel.*`, `featured_image_field.*`).
136
+ - Pluralisation is handled by the caller via `pluralSuffix`: German "Ergebnis" + "se", Swedish
137
+ "resultat" is invariant.
138
+
139
+ **`locale` prop (optional, default `"en"`)** on: `PostCard`, `FeaturedPostCard`,
140
+ `EditorialPostCard`, `PostHero`, `BlogSearch`, `RelatedPosts`, `FaqSection`, `TableOfContents`,
141
+ `AuthorBio`, `PostForm`, and `BlogIndexLayoutProps` (both `/blog` layouts). Components no longer
142
+ import `createPackageT` for these strings. Explicit props (`title`, `placeholder`) still win over
143
+ the locale default.
144
+
145
+ **Dates: `formatPostDate(iso, style, locale)`** — the `en-US` hardcode in `post-card.tsx` and
146
+ `post-hero.tsx` is gone; both now go through `formatPostDate`. Locale maps to `en-US`, `de-DE`,
147
+ `sv-SE`. Omitted or unknown locales produce byte-identical English output (`"Jan 5, 2026"`).
148
+
149
+ **`PostForm` toolbar labels** (`YT`, `Note`, `Img`) are now catalog keys. A custom `embeds` array
150
+ still overrides the defaults entirely.
151
+
152
+ **Sealed pages: `BlogConfig.uiLocale`** (optional, default `"en"`, resolved to the same key on
153
+ `ResolvedBlogConfig`) drives chrome text and dates in `createBlogIndexPage`,
154
+ `createBlogPostPage`, `createBlogTagPage`, and the admin list/new/edit pages. The admin pages'
155
+ previously hardcoded English ("Blog posts", "+ New post", table headers, "Edit: …") are catalog
156
+ keys. This is distinct from `defaultLocale`, which stays the content/JSON-LD locale.
157
+
158
+ **Back-compat: zero break.** Every new prop and config field is optional, and the default output
159
+ for every component is unchanged. `formatPostDate`'s new `locale` argument is last and optional.
160
+
161
+ **Not yet locale-aware (still English-only, follow-up):** the `./ui` admin tree
162
+ (`BlogAdminPanel`, `PostListTable`, `LayoutSelector`, `PostPreview`, `PanelDialog`),
163
+ `FeaturedImageField`, the `YouTube` MDX embed, and `BlogContent`'s render-error text. These still
164
+ use `createPackageT` and need the same treatment before a German admin is usable end to end.
165
+
166
+ **Tests:** `src/__tests__/i18n.test.ts` (translator, catalog parity and placeholder integrity,
167
+ plurals, date formatting per locale) and `src/__tests__/locale-components.test.ts`
168
+ (react-dom/server render of the locale prop on FAQ, TOC, hero and card). New autotest cases in
169
+ `test-app/app/autotest/hazo_blog_i18n_scenarios.ts` cover catalog parity and the locale-aware
170
+ translator and dates.
171
+
6
172
  ## 1.3.0 — 2026-09-20
7
173
 
8
174
  **Migrated onto the canonical `hazo_core/i18n` bridge**
package/README.md CHANGED
@@ -259,6 +259,17 @@ await repo.getPostBySlug(slug, { includeUnpublished: true });
259
259
 
260
260
  — and `getAllSlugs()` now returns `Array<{ slug, locale }>` instead of `string[]`. Existing single-locale data needs no migration: every current row's `locale` is `NULL` already, which *is* the default-locale bucket every existing query path reads from.
261
261
 
262
+ ## UI locale (chrome text and dates)
263
+
264
+ Separate from the content locale above, the package's own UI text (headings, labels, empty states, admin pages, search placeholder) and date formatting follow a **UI locale**. Shipped catalogs: `en` (default), `de`, `sv` (`src/locales/*.json`, identical key sets). Any other value falls back to English text.
265
+
266
+ - **Config:** `BlogConfig.uiLocale` (default `"en"`) drives the sealed page factories (`createBlogIndexPage`, `createBlogPostPage`, `createBlogTagPage`, admin pages). Build one factory per UI locale if your site serves several.
267
+ - **Components:** the public presentational components accept an optional `locale` prop (default `"en"`): `PostCard`, `FeaturedPostCard`, `EditorialPostCard`, `PostHero`, `BlogSearch`, `RelatedPosts`, `FaqSection`, `TableOfContents`, `AuthorBio`, `PostForm` (toolbar labels included), and `BlogIndexLayoutProps.locale` for the `/blog` layouts.
268
+ - **Dates:** `formatPostDate(iso, style, locale)` uses the locale's Intl conventions (`en-US`, `de-DE`, `sv-SE`).
269
+ - **Translator:** `blogTranslator(locale)` (exported from `hazo_blog/lib`) returns a per-call `t()` bound to that catalog, with English fallback. `pluralSuffix(locale, count)` covers the one plural-dependent key.
270
+
271
+ Not yet locale-aware (still English-only): the `./ui` admin panel tree (`BlogAdminPanel`, `PostListTable`, `LayoutSelector`, `PostPreview`, `PanelDialog`), `FeaturedImageField`, the MDX `YouTube` embed, and `BlogContent`'s error text.
272
+
262
273
  ## Layouts
263
274
 
264
275
  `config.defaultIndexLayout` (and the runtime override in [Settings](#settings))
@@ -64,6 +64,7 @@ CREATE TABLE IF NOT EXISTS hazo_blog_posts (
64
64
  publish_date TIMESTAMPTZ,
65
65
  locale TEXT DEFAULT NULL, -- BCP-47 locale; NULL = default locale
66
66
  translation_of UUID REFERENCES hazo_blog_posts(id) ON DELETE SET NULL, -- root post of this translation cluster
67
+ review_notes TEXT, -- BlogService#reviewTranslatedPost()'s LLM notes (nullable; never flips status)
67
68
  indexable BOOLEAN NOT NULL DEFAULT TRUE, -- FALSE excludes from sitemap/robots
68
69
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
69
70
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
@@ -71,6 +72,7 @@ CREATE TABLE IF NOT EXISTS hazo_blog_posts (
71
72
  ALTER TABLE hazo_blog_posts ADD COLUMN IF NOT EXISTS locale TEXT DEFAULT NULL;
72
73
  ALTER TABLE hazo_blog_posts ADD COLUMN IF NOT EXISTS translation_of UUID REFERENCES hazo_blog_posts(id) ON DELETE SET NULL;
73
74
  ALTER TABLE hazo_blog_posts ADD COLUMN IF NOT EXISTS indexable BOOLEAN NOT NULL DEFAULT TRUE;
75
+ ALTER TABLE hazo_blog_posts ADD COLUMN IF NOT EXISTS review_notes TEXT;
74
76
  -- FR-034: locale-aware uniqueness (see categories index comment above).
75
77
  DROP INDEX IF EXISTS idx_hazo_blog_posts_scope_slug;
76
78
  CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_blog_posts_scope_slug_locale
@@ -70,6 +70,7 @@ CREATE TABLE IF NOT EXISTS hazo_blog_posts (
70
70
  publish_date TEXT,
71
71
  locale TEXT DEFAULT NULL, -- BCP-47 locale; NULL = default locale
72
72
  translation_of TEXT REFERENCES hazo_blog_posts(id) ON DELETE SET NULL, -- root post of this translation cluster
73
+ review_notes TEXT, -- BlogService#reviewTranslatedPost()'s LLM notes (nullable; never flips status)
73
74
  indexable INTEGER NOT NULL DEFAULT 1, -- 0/1; FALSE excludes from sitemap/robots
74
75
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
75
76
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
@@ -8,8 +8,10 @@ export interface PostFormProps {
8
8
  endpoint?: string;
9
9
  /** Called after a successful save with the server response. */
10
10
  onSaved?: (result: unknown) => void;
11
- /** MDX embeds offered in the editor toolbar (host can extend). */
11
+ /** MDX embeds offered in the editor toolbar (host can extend). Default: YouTube / Note / Image, labelled for `locale`. */
12
12
  embeds?: MarkdownEmbed[];
13
+ /** UI locale for user-visible text, toolbar labels and dates. Default "en". */
14
+ locale?: string;
13
15
  /**
14
16
  * Paste-to-upload handler wired to hazo_files by the host. Takes
15
17
  * precedence over `imageUploadEndpoint` when both are provided (back-compat
@@ -28,5 +30,5 @@ export interface PostFormProps {
28
30
  onCancel?: () => void;
29
31
  className?: string;
30
32
  }
31
- export declare function PostForm({ post, categories, endpoint, onSaved, embeds, onImageUpload, imageUploadEndpoint, fetchFn, onCancel, className, }: PostFormProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function PostForm({ post, categories, endpoint, onSaved, embeds, onImageUpload, imageUploadEndpoint, fetchFn, onCancel, className, locale: uiLocale, }: PostFormProps): import("react/jsx-runtime").JSX.Element;
32
34
  //# sourceMappingURL=post-form.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"post-form.d.ts","sourceRoot":"","sources":["../../../src/components/admin/post-form.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,KAAK,EACV,YAAY,EACZ,qBAAqB,EAGtB,MAAM,sBAAsB,CAAC;AAQ9B,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,IAAI,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,kEAAkE;IAClE,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvD;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClE,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgBD,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,UAAe,EACf,QAA4B,EAC5B,OAAO,EACP,MAAuB,EACvB,aAAa,EACb,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE,aAAa,2CAyQf"}
1
+ {"version":3,"file":"post-form.d.ts","sourceRoot":"","sources":["../../../src/components/admin/post-form.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,KAAK,EACV,YAAY,EACZ,qBAAqB,EAGtB,MAAM,sBAAsB,CAAC;AAK9B,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,IAAI,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,0HAA0H;IAC1H,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;IACzB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvD;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClE,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsBD,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,UAAe,EACf,QAA4B,EAC5B,OAAO,EACP,MAAM,EACN,aAAa,EACb,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,SAAS,EACT,MAAM,EAAE,QAAe,GACxB,EAAE,aAAa,2CA2Qf"}
@@ -6,23 +6,29 @@ import { cn } from "hazo_ui/utils";
6
6
  import { slugify } from "../../lib/text.js";
7
7
  import { createBlogImageUploader } from "./upload-image.js";
8
8
  import { FeaturedImageField } from "./featured-image-field.js";
9
- import en from "../../locales/en.json" with { type: "json" };
10
- import { createPackageT } from "hazo_core/i18n";
11
- const t = createPackageT("hazo_blog", en);
12
- const DEFAULT_EMBEDS = [
13
- { name: "youtube", label: "YT", snippet: '\n<YouTube id="VIDEO_ID" title="" />\n' },
14
- {
15
- name: "callout",
16
- label: "Note",
17
- snippet: (sel) => `\n<Callout type="info">\n${sel || "..."}\n</Callout>\n`,
18
- },
19
- {
20
- name: "image",
21
- label: "Img",
22
- snippet: '\n<BlogImage src="/path.jpg" alt="" caption="" />\n',
23
- },
24
- ];
25
- export function PostForm({ post, categories = [], endpoint = "/api/admin/blog", onSaved, embeds = DEFAULT_EMBEDS, onImageUpload, imageUploadEndpoint, fetchFn, onCancel, className, }) {
9
+ import { blogTranslator } from "../../lib/i18n.js";
10
+ function buildDefaultEmbeds(t) {
11
+ return [
12
+ {
13
+ name: "youtube",
14
+ label: t("post_form.embed_youtube_label"),
15
+ snippet: '\n<YouTube id="VIDEO_ID" title="" />\n',
16
+ },
17
+ {
18
+ name: "callout",
19
+ label: t("post_form.embed_note_label"),
20
+ snippet: (sel) => `\n<Callout type="info">\n${sel || "..."}\n</Callout>\n`,
21
+ },
22
+ {
23
+ name: "image",
24
+ label: t("post_form.embed_image_label"),
25
+ snippet: '\n<BlogImage src="/path.jpg" alt="" caption="" />\n',
26
+ },
27
+ ];
28
+ }
29
+ export function PostForm({ post, categories = [], endpoint = "/api/admin/blog", onSaved, embeds, onImageUpload, imageUploadEndpoint, fetchFn, onCancel, className, locale: uiLocale = "en", }) {
30
+ const t = blogTranslator(uiLocale);
31
+ const toolbarEmbeds = useMemo(() => embeds ?? buildDefaultEmbeds(t), [embeds, uiLocale]);
26
32
  // Reuse an existing `onImageUpload` if the caller already wired one
27
33
  // (back-compat); otherwise build one from `imageUploadEndpoint` if given;
28
34
  // otherwise `undefined` (URL-only mode, no upload UI).
@@ -92,5 +98,5 @@ export function PostForm({ post, categories = [], endpoint = "/api/admin/blog",
92
98
  return (_jsxs("form", { className: cn("space-y-5", className), onSubmit: (e) => e.preventDefault(), children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.title_label") }), _jsx("input", { className: inputCls, value: title, onChange: (e) => setTitle(e.target.value), placeholder: t("post_form.title_placeholder") })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.slug_label") }), _jsx("input", { className: inputCls, value: slugTouched ? slug : effectiveSlug(), onChange: (e) => {
93
99
  setSlugTouched(true);
94
100
  setSlug(e.target.value);
95
- }, placeholder: t("post_form.slug_placeholder") })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.content_label") }), _jsx(MarkdownEditor, { value: content, onChange: setContent, height: 480, embeds: embeds, onImageUpload: uploader, placeholder: t("post_form.content_placeholder") })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.category_label") }), _jsxs("select", { className: inputCls, value: categoryId, onChange: (e) => setCategoryId(e.target.value), children: [_jsx("option", { value: "", children: t("post_form.category_none_option") }), categories.map((c) => (_jsx("option", { value: c.id, children: c.name }, c.id)))] })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.tags_label") }), _jsx("input", { className: inputCls, value: tags, onChange: (e) => setTags(e.target.value), placeholder: t("post_form.tags_placeholder") })] })] }), _jsx(FeaturedImageField, { value: featuredImage, onChange: setFeaturedImage, uploader: uploader }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.locale_label") }), _jsx("input", { className: inputCls, value: locale, onChange: (e) => setLocale(e.target.value), placeholder: t("post_form.locale_placeholder") })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.translation_of_label") }), _jsx("input", { className: inputCls, value: translationOf, onChange: (e) => setTranslationOf(e.target.value), placeholder: t("post_form.translation_of_placeholder") })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: "post-form-indexable", type: "checkbox", className: "h-4 w-4 rounded border-border accent-primary", checked: indexable, onChange: (e) => setIndexable(e.target.checked) }), _jsx("label", { htmlFor: "post-form-indexable", className: "text-sm font-medium", children: t("post_form.indexable_label") })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.meta_title_label") }), _jsx("input", { className: inputCls, value: metaTitle, onChange: (e) => setMetaTitle(e.target.value) })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.meta_description_label") }), _jsx("input", { className: inputCls, value: metaDescription, onChange: (e) => setMetaDescription(e.target.value) })] })] }), _jsxs("fieldset", { className: "rounded-lg border border-border p-4", children: [_jsx("legend", { className: "px-1 text-sm font-medium", children: t("post_form.faq_legend") }), faq.map((item, i) => (_jsxs("div", { className: "mb-2 grid gap-2 sm:grid-cols-2", children: [_jsx("input", { className: inputCls, value: item.question, placeholder: t("post_form.faq_question_placeholder"), onChange: (e) => setFaq((prev) => prev.map((f, j) => (j === i ? { ...f, question: e.target.value } : f))) }), _jsx("input", { className: inputCls, value: item.answer, placeholder: t("post_form.faq_answer_placeholder"), onChange: (e) => setFaq((prev) => prev.map((f, j) => (j === i ? { ...f, answer: e.target.value } : f))) })] }, i))), _jsx("button", { type: "button", className: "mt-1 text-sm text-primary hover:underline", onClick: () => setFaq((p) => [...p, { question: "", answer: "" }]), children: t("post_form.faq_add_item") })] }), error && _jsx("p", { className: "text-sm text-destructive", children: error }), _jsxs("div", { className: "flex gap-3", children: [onCancel && (_jsx("button", { type: "button", disabled: saving, onClick: onCancel, className: "rounded-lg border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50", children: t("post_form.cancel") })), _jsx("button", { type: "button", disabled: saving, onClick: () => save("draft"), className: "rounded-lg border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50", children: t("post_form.save_draft") }), _jsx("button", { type: "button", disabled: saving, onClick: () => save("published"), className: "rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50", children: saving ? t("post_form.saving") : t("post_form.publish") })] })] }));
101
+ }, placeholder: t("post_form.slug_placeholder") })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.content_label") }), _jsx(MarkdownEditor, { value: content, onChange: setContent, height: 480, embeds: toolbarEmbeds, onImageUpload: uploader, placeholder: t("post_form.content_placeholder") })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.category_label") }), _jsxs("select", { className: inputCls, value: categoryId, onChange: (e) => setCategoryId(e.target.value), children: [_jsx("option", { value: "", children: t("post_form.category_none_option") }), categories.map((c) => (_jsx("option", { value: c.id, children: c.name }, c.id)))] })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.tags_label") }), _jsx("input", { className: inputCls, value: tags, onChange: (e) => setTags(e.target.value), placeholder: t("post_form.tags_placeholder") })] })] }), _jsx(FeaturedImageField, { value: featuredImage, onChange: setFeaturedImage, uploader: uploader }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.locale_label") }), _jsx("input", { className: inputCls, value: locale, onChange: (e) => setLocale(e.target.value), placeholder: t("post_form.locale_placeholder") })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.translation_of_label") }), _jsx("input", { className: inputCls, value: translationOf, onChange: (e) => setTranslationOf(e.target.value), placeholder: t("post_form.translation_of_placeholder") })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: "post-form-indexable", type: "checkbox", className: "h-4 w-4 rounded border-border accent-primary", checked: indexable, onChange: (e) => setIndexable(e.target.checked) }), _jsx("label", { htmlFor: "post-form-indexable", className: "text-sm font-medium", children: t("post_form.indexable_label") })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.meta_title_label") }), _jsx("input", { className: inputCls, value: metaTitle, onChange: (e) => setMetaTitle(e.target.value) })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("post_form.meta_description_label") }), _jsx("input", { className: inputCls, value: metaDescription, onChange: (e) => setMetaDescription(e.target.value) })] })] }), _jsxs("fieldset", { className: "rounded-lg border border-border p-4", children: [_jsx("legend", { className: "px-1 text-sm font-medium", children: t("post_form.faq_legend") }), faq.map((item, i) => (_jsxs("div", { className: "mb-2 grid gap-2 sm:grid-cols-2", children: [_jsx("input", { className: inputCls, value: item.question, placeholder: t("post_form.faq_question_placeholder"), onChange: (e) => setFaq((prev) => prev.map((f, j) => (j === i ? { ...f, question: e.target.value } : f))) }), _jsx("input", { className: inputCls, value: item.answer, placeholder: t("post_form.faq_answer_placeholder"), onChange: (e) => setFaq((prev) => prev.map((f, j) => (j === i ? { ...f, answer: e.target.value } : f))) })] }, i))), _jsx("button", { type: "button", className: "mt-1 text-sm text-primary hover:underline", onClick: () => setFaq((p) => [...p, { question: "", answer: "" }]), children: t("post_form.faq_add_item") })] }), error && _jsx("p", { className: "text-sm text-destructive", children: error }), _jsxs("div", { className: "flex gap-3", children: [onCancel && (_jsx("button", { type: "button", disabled: saving, onClick: onCancel, className: "rounded-lg border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50", children: t("post_form.cancel") })), _jsx("button", { type: "button", disabled: saving, onClick: () => save("draft"), className: "rounded-lg border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50", children: t("post_form.save_draft") }), _jsx("button", { type: "button", disabled: saving, onClick: () => save("published"), className: "rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50", children: saving ? t("post_form.saving") : t("post_form.publish") })] })] }));
96
102
  }
@@ -2,6 +2,8 @@ import type { AuthorInfo } from "../types/index.js";
2
2
  export interface AuthorBioProps {
3
3
  author: AuthorInfo;
4
4
  className?: string;
5
+ /** UI locale for user-visible text. Default "en". */
6
+ locale?: string;
5
7
  }
6
- export declare function AuthorBio({ author, className }: AuthorBioProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function AuthorBio({ author, className, locale }: AuthorBioProps): import("react/jsx-runtime").JSX.Element;
7
9
  //# sourceMappingURL=author-bio.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"author-bio.d.ts","sourceRoot":"","sources":["../../src/components/author-bio.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMpD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,cAAc,2CA0B9D"}
1
+ {"version":3,"file":"author-bio.d.ts","sourceRoot":"","sources":["../../src/components/author-bio.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAa,EAAE,EAAE,cAAc,2CA2B7E"}
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Image from "next/image";
3
3
  import { cn } from "hazo_ui/utils";
4
- import en from "../locales/en.json" with { type: "json" };
5
- import { createPackageT } from "hazo_core/i18n";
6
- const t = createPackageT("hazo_blog", en);
7
- export function AuthorBio({ author, className }) {
4
+ import { blogTranslator } from "../lib/i18n.js";
5
+ export function AuthorBio({ author, className, locale = "en" }) {
6
+ const t = blogTranslator(locale);
8
7
  return (_jsxs("aside", { className: cn("my-8 flex items-center gap-4 rounded-xl border border-border bg-muted/30 p-5", className), children: [author.avatar && (_jsx(Image, { src: author.avatar, alt: author.name, width: 56, height: 56, className: "h-14 w-14 rounded-full object-cover" })), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("author_bio.written_by") }), _jsx("a", { href: author.url, className: "font-semibold text-foreground hover:text-primary", children: author.name }), author.bio && _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: author.bio })] })] }));
9
8
  }
@@ -6,6 +6,8 @@ export interface BlogSearchProps {
6
6
  className?: string;
7
7
  /** Visual style. "default" preserves the original boxy input; "pill" renders a rounded, icon-prefixed input. */
8
8
  variant?: "default" | "pill";
9
+ /** UI locale for the placeholder. Default "en". An explicit `placeholder` always wins. */
10
+ locale?: string;
9
11
  }
10
- export declare function BlogSearch({ endpoint, basePath, placeholder, className, variant, }: BlogSearchProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function BlogSearch({ endpoint, basePath, placeholder, className, variant, locale, }: BlogSearchProps): import("react/jsx-runtime").JSX.Element;
11
13
  //# sourceMappingURL=blog-search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-search.d.ts","sourceRoot":"","sources":["../../src/components/blog-search.tsx"],"names":[],"mappings":"AAmBA,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gHAAgH;IAChH,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;CAC9B;AAED,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,QAAkB,EAClB,WAA0C,EAC1C,SAAS,EACT,OAAmB,GACpB,EAAE,eAAe,2CAyFjB"}
1
+ {"version":3,"file":"blog-search.d.ts","sourceRoot":"","sources":["../../src/components/blog-search.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gHAAgH;IAChH,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC7B,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,QAAkB,EAClB,WAAW,EACX,SAAS,EACT,OAAmB,EACnB,MAAa,GACd,EAAE,eAAe,2CA0FjB"}
@@ -6,10 +6,9 @@ import Link from "next/link";
6
6
  import { cn } from "hazo_ui/utils";
7
7
  import { trackBlogEvent } from "../lib/analytics.js";
8
8
  import { SearchIcon } from "./icons.js";
9
- import en from "../locales/en.json" with { type: "json" };
10
- import { createPackageT } from "hazo_core/i18n";
11
- const t = createPackageT("hazo_blog", en);
12
- export function BlogSearch({ endpoint, basePath = "/blog", placeholder = t("blog_search.placeholder"), className, variant = "default", }) {
9
+ import { blogTranslator } from "../lib/i18n.js";
10
+ export function BlogSearch({ endpoint, basePath = "/blog", placeholder, className, variant = "default", locale = "en", }) {
11
+ const t = blogTranslator(locale);
13
12
  const url = endpoint ?? `${basePath}/api/search`;
14
13
  const [query, setQuery] = useState("");
15
14
  const [results, setResults] = useState([]);
@@ -63,7 +62,7 @@ export function BlogSearch({ endpoint, basePath = "/blog", placeholder = t("blog
63
62
  setResults([]);
64
63
  }
65
64
  }
66
- return (_jsxs("div", { className: cn("relative", className), children: [variant === "pill" && (_jsx(SearchIcon, { className: "pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" })), _jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, className: variant === "pill"
65
+ return (_jsxs("div", { className: cn("relative", className), children: [variant === "pill" && (_jsx(SearchIcon, { className: "pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" })), _jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder ?? t("blog_search.placeholder"), className: variant === "pill"
67
66
  ? "w-full rounded-full border border-border bg-background py-2.5 pl-11 pr-4 text-sm outline-none focus:ring-2 focus:ring-primary"
68
67
  : "w-full rounded-lg border border-border bg-background px-4 py-2 text-sm outline-none focus:ring-2 focus:ring-primary" }), open && results.length > 0 && (_jsx("ul", { className: "absolute z-50 mt-1 max-h-80 w-full overflow-auto rounded-lg border border-border bg-background shadow-xl", children: results.map((r) => (_jsx("li", { className: "border-b border-border last:border-0", children: _jsxs(Link, { href: `${basePath}/${r.slug}`, className: "block px-4 py-3 text-sm hover:bg-accent", onClick: () => setOpen(false), children: [_jsx("span", { className: "font-medium text-foreground", children: r.title }), r.excerpt && (_jsx("span", { className: "block truncate text-xs text-muted-foreground", children: r.excerpt }))] }) }, r.slug))) }))] }));
69
68
  }
@@ -5,6 +5,8 @@ export interface EditorialPostCardProps {
5
5
  index: number;
6
6
  basePath?: string;
7
7
  className?: string;
8
+ /** UI locale for dates. Default "en". */
9
+ locale?: string;
8
10
  }
9
- export declare function EditorialPostCard({ post, index, basePath, className, }: EditorialPostCardProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function EditorialPostCard({ post, index, basePath, className, locale, }: EditorialPostCardProps): import("react/jsx-runtime").JSX.Element;
10
12
  //# sourceMappingURL=editorial-post-card.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"editorial-post-card.d.ts","sourceRoot":"","sources":["../../src/components/editorial-post-card.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAK/D,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,KAAK,EACL,QAAkB,EAClB,SAAS,GACV,EAAE,sBAAsB,2CA+CxB"}
1
+ {"version":3,"file":"editorial-post-card.d.ts","sourceRoot":"","sources":["../../src/components/editorial-post-card.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAK/D,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,KAAK,EACL,QAAkB,EAClB,SAAS,EACT,MAAa,GACd,EAAE,sBAAsB,2CA+CxB"}
@@ -11,10 +11,10 @@ import { ImageIcon } from "./icons.js";
11
11
  import { formatPostDate } from "../lib/dates.js";
12
12
  const ASPECT_BY_POSITION = ["aspect-[4/5]", "aspect-[16/10]", "aspect-[4/3]"];
13
13
  const OFFSET_BY_POSITION = ["", "", "lg:mt-6"];
14
- export function EditorialPostCard({ post, index, basePath = "/blog", className, }) {
14
+ export function EditorialPostCard({ post, index, basePath = "/blog", className, locale = "en", }) {
15
15
  const position = index % 3;
16
16
  return (_jsx("article", { className: cn(OFFSET_BY_POSITION[position], className), children: _jsxs(Link, { href: `${basePath}/${post.slug}`, className: "group block", children: [_jsx("div", { className: cn("relative w-full overflow-hidden rounded-2xl", ASPECT_BY_POSITION[position]), children: post.featured_image ? (_jsx(Image, { src: post.featured_image, alt: post.title, fill: true, className: "object-cover transition-transform duration-300 group-hover:scale-105", sizes: "(max-width: 768px) 100vw, 33vw" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-muted to-muted/50", children: _jsx(ImageIcon, { className: "h-10 w-10 text-muted-foreground/40" }) })) }), _jsxs("div", { className: "mt-4", children: [post.category && (_jsx("span", { className: "mb-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-medium", style: {
17
17
  backgroundColor: `${post.category.colour}20`,
18
18
  color: post.category.colour,
19
- }, children: post.category.name })), _jsx("h3", { className: "line-clamp-2 text-lg font-semibold text-foreground group-hover:text-primary", children: post.title }), _jsx("time", { className: "mt-2 block text-xs text-muted-foreground", children: formatPostDate(post.publish_date, "short") })] })] }) }));
19
+ }, children: post.category.name })), _jsx("h3", { className: "line-clamp-2 text-lg font-semibold text-foreground group-hover:text-primary", children: post.title }), _jsx("time", { className: "mt-2 block text-xs text-muted-foreground", children: formatPostDate(post.publish_date, "short", locale) })] })] }) }));
20
20
  }
@@ -3,7 +3,9 @@ export interface FaqSectionProps {
3
3
  faq: FaqItem[];
4
4
  title?: string;
5
5
  className?: string;
6
+ /** UI locale for the default heading. Default "en". */
7
+ locale?: string;
6
8
  }
7
9
  /** Renders FAQ as a no-JS <details> accordion (the FAQPage JSON-LD is emitted separately). */
8
- export declare function FaqSection({ faq, title, className }: FaqSectionProps): import("react/jsx-runtime").JSX.Element | null;
10
+ export declare function FaqSection({ faq, title, className, locale }: FaqSectionProps): import("react/jsx-runtime").JSX.Element | null;
9
11
  //# sourceMappingURL=faq-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"faq-section.d.ts","sourceRoot":"","sources":["../../src/components/faq-section.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAMjD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,OAAO,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,KAA8B,EAAE,SAAS,EAAE,EAAE,eAAe,kDAiB7F"}
1
+ {"version":3,"file":"faq-section.d.ts","sourceRoot":"","sources":["../../src/components/faq-section.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGjD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,OAAO,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,MAAa,EAAE,EAAE,eAAe,kDAkBnF"}
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "hazo_ui/utils";
3
- import en from "../locales/en.json" with { type: "json" };
4
- import { createPackageT } from "hazo_core/i18n";
5
- const t = createPackageT("hazo_blog", en);
3
+ import { blogTranslator } from "../lib/i18n.js";
6
4
  /** Renders FAQ as a no-JS <details> accordion (the FAQPage JSON-LD is emitted separately). */
7
- export function FaqSection({ faq, title = t("faq_section.title"), className }) {
5
+ export function FaqSection({ faq, title, className, locale = "en" }) {
8
6
  if (!faq || faq.length === 0)
9
7
  return null;
10
- return (_jsxs("section", { className: cn("my-8", className), children: [_jsx("h2", { className: "mb-4 text-2xl font-bold text-foreground", children: title }), _jsx("dl", { className: "divide-y divide-border rounded-xl border border-border", children: faq.map((item, i) => (_jsxs("details", { className: "group p-4", children: [_jsx("summary", { className: "cursor-pointer list-none font-medium text-foreground", children: _jsx("dt", { className: "inline", children: item.question }) }), _jsx("dd", { className: "mt-2 text-sm text-muted-foreground", children: item.answer })] }, i))) })] }));
8
+ const t = blogTranslator(locale);
9
+ return (_jsxs("section", { className: cn("my-8", className), children: [_jsx("h2", { className: "mb-4 text-2xl font-bold text-foreground", children: title ?? t("faq_section.title") }), _jsx("dl", { className: "divide-y divide-border rounded-xl border border-border", children: faq.map((item, i) => (_jsxs("details", { className: "group p-4", children: [_jsx("summary", { className: "cursor-pointer list-none font-medium text-foreground", children: _jsx("dt", { className: "inline", children: item.question }) }), _jsx("dd", { className: "mt-2 text-sm text-muted-foreground", children: item.answer })] }, i))) })] }));
11
10
  }
@@ -3,6 +3,8 @@ export interface FeaturedPostCardProps {
3
3
  post: BlogPostWithRelations;
4
4
  basePath?: string;
5
5
  className?: string;
6
+ /** UI locale for user-visible text and dates. Default "en". */
7
+ locale?: string;
6
8
  }
7
- export declare function FeaturedPostCard({ post, basePath, className }: FeaturedPostCardProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function FeaturedPostCard({ post, basePath, className, locale, }: FeaturedPostCardProps): import("react/jsx-runtime").JSX.Element;
8
10
  //# sourceMappingURL=featured-post-card.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"featured-post-card.d.ts","sourceRoot":"","sources":["../../src/components/featured-post-card.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAM/D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,QAAkB,EAAE,SAAS,EAAE,EAAE,qBAAqB,2CA+D9F"}
1
+ {"version":3,"file":"featured-post-card.d.ts","sourceRoot":"","sources":["../../src/components/featured-post-card.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,QAAkB,EAClB,SAAS,EACT,MAAa,GACd,EAAE,qBAAqB,2CAgEvB"}
@@ -7,12 +7,11 @@ import Image from "next/image";
7
7
  import { cn } from "hazo_ui/utils";
8
8
  import { ClockIcon, ArrowRightIcon, ImageIcon } from "./icons.js";
9
9
  import { formatPostDate } from "../lib/dates.js";
10
- import en from "../locales/en.json" with { type: "json" };
11
- import { createPackageT } from "hazo_core/i18n";
12
- const t = createPackageT("hazo_blog", en);
13
- export function FeaturedPostCard({ post, basePath = "/blog", className }) {
10
+ import { blogTranslator } from "../lib/i18n.js";
11
+ export function FeaturedPostCard({ post, basePath = "/blog", className, locale = "en", }) {
12
+ const t = blogTranslator(locale);
14
13
  return (_jsxs("article", { className: cn("grid overflow-hidden rounded-3xl border border-border bg-card md:grid-cols-[3fr_2fr]", className), children: [_jsx("div", { className: "relative aspect-video w-full overflow-hidden md:aspect-auto md:h-full", children: post.featured_image ? (_jsx(Image, { src: post.featured_image, alt: post.title, fill: true, className: "object-cover", sizes: "(max-width: 768px) 100vw, 60vw", priority: true })) : (_jsx("div", { className: "flex h-full min-h-48 w-full items-center justify-center bg-gradient-to-br from-muted to-muted/50", children: _jsx(ImageIcon, { className: "h-12 w-12 text-muted-foreground/40" }) })) }), _jsxs("div", { className: "flex flex-col justify-center gap-4 p-6 sm:p-8 lg:p-10", children: [_jsxs("div", { className: "flex items-center gap-3 text-xs font-medium text-muted-foreground", children: [post.category && (_jsx("span", { className: "inline-block rounded-full px-2.5 py-0.5 text-xs font-medium", style: {
15
14
  backgroundColor: `${post.category.colour}20`,
16
15
  color: post.category.colour,
17
- }, children: post.category.name })), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(ClockIcon, { className: "h-3.5 w-3.5" }), t("featured_post_card.reading_time", { minutes: String(post.reading_time) })] })] }), _jsx("h2", { className: "text-2xl font-bold leading-tight text-foreground sm:text-3xl lg:text-4xl", children: _jsx(Link, { href: `${basePath}/${post.slug}`, className: "hover:text-primary", children: post.title }) }), post.excerpt && (_jsx("p", { className: "line-clamp-3 text-base text-muted-foreground", children: post.excerpt })), _jsxs("div", { className: "mt-2 flex items-center justify-between", children: [_jsx("time", { className: "text-sm text-muted-foreground", children: formatPostDate(post.publish_date) }), _jsx(Link, { href: `${basePath}/${post.slug}`, "aria-label": t("featured_post_card.read_aria", { title: post.title }), className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-border text-foreground transition-colors hover:bg-accent", children: _jsx(ArrowRightIcon, { className: "h-5 w-5" }) })] })] })] }));
16
+ }, children: post.category.name })), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(ClockIcon, { className: "h-3.5 w-3.5" }), t("featured_post_card.reading_time", { minutes: String(post.reading_time) })] })] }), _jsx("h2", { className: "text-2xl font-bold leading-tight text-foreground sm:text-3xl lg:text-4xl", children: _jsx(Link, { href: `${basePath}/${post.slug}`, className: "hover:text-primary", children: post.title }) }), post.excerpt && (_jsx("p", { className: "line-clamp-3 text-base text-muted-foreground", children: post.excerpt })), _jsxs("div", { className: "mt-2 flex items-center justify-between", children: [_jsx("time", { className: "text-sm text-muted-foreground", children: formatPostDate(post.publish_date, "short", locale) }), _jsx(Link, { href: `${basePath}/${post.slug}`, "aria-label": t("featured_post_card.read_aria", { title: post.title }), className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-border text-foreground transition-colors hover:bg-accent", children: _jsx(ArrowRightIcon, { className: "h-5 w-5" }) })] })] })] }));
18
17
  }
@@ -1,3 +1,3 @@
1
1
  import type { BlogIndexLayoutProps } from "./index.js";
2
- export declare function BlogIndexEditorial({ posts, basePath, searchEndpoint, query, page, totalPages, title, description, }: BlogIndexLayoutProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function BlogIndexEditorial({ posts, basePath, searchEndpoint, query, page, totalPages, title, description, locale, }: BlogIndexLayoutProps): import("react/jsx-runtime").JSX.Element;
3
3
  //# sourceMappingURL=blog-index-editorial.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-index-editorial.d.ts","sourceRoot":"","sources":["../../../src/components/layouts/blog-index-editorial.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAMvD,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,cAAc,EACd,KAAK,EACL,IAAI,EACJ,UAAU,EACV,KAAsC,EACtC,WAAW,GACZ,EAAE,oBAAoB,2CA+EtB"}
1
+ {"version":3,"file":"blog-index-editorial.d.ts","sourceRoot":"","sources":["../../../src/components/layouts/blog-index-editorial.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,cAAc,EACd,KAAK,EACL,IAAI,EACJ,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAa,GACd,EAAE,oBAAoB,2CAiFtB"}
@@ -13,18 +13,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  import { FeaturedPostCard } from "../featured-post-card.js";
14
14
  import { EditorialPostCard } from "../editorial-post-card.js";
15
15
  import { BlogSearch } from "../blog-search.js";
16
- import en from "../../locales/en.json" with { type: "json" };
17
- import { createPackageT } from "hazo_core/i18n";
18
- const t = createPackageT("hazo_blog", en);
19
- export function BlogIndexEditorial({ posts, basePath, searchEndpoint, query, page, totalPages, title = t("common.default_blog_title"), description, }) {
16
+ import { blogTranslator, pluralSuffix } from "../../lib/i18n.js";
17
+ export function BlogIndexEditorial({ posts, basePath, searchEndpoint, query, page, totalPages, title, description, locale = "en", }) {
18
+ const t = blogTranslator(locale);
20
19
  const showFeatured = !query && posts.length > 0;
21
20
  const featured = showFeatured ? posts[0] : null;
22
21
  const gridPosts = showFeatured ? posts.slice(1) : posts;
23
- return (_jsxs("div", { className: "mx-auto max-w-6xl px-4 py-10", children: [_jsxs("div", { className: "mb-8 flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-4xl font-bold text-foreground sm:text-5xl", children: title }), description && (_jsx("p", { className: "mt-3 max-w-xl text-base text-muted-foreground", children: description })), query && (_jsxs("p", { className: "mt-3 text-sm text-muted-foreground", children: [t("common.results_count", {
22
+ return (_jsxs("div", { className: "mx-auto max-w-6xl px-4 py-10", children: [_jsxs("div", { className: "mb-8 flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-4xl font-bold text-foreground sm:text-5xl", children: title ?? t("common.default_blog_title") }), description && (_jsx("p", { className: "mt-3 max-w-xl text-base text-muted-foreground", children: description })), query && (_jsxs("p", { className: "mt-3 text-sm text-muted-foreground", children: [t("common.results_count", {
24
23
  count: String(posts.length),
25
- plural: posts.length !== 1 ? "s" : "",
24
+ plural: pluralSuffix(locale, posts.length),
26
25
  query,
27
- }), "\u00A0\u00B7\u00A0", _jsx("a", { href: basePath, className: "text-primary hover:underline", children: t("common.clear") })] }))] }), _jsx(BlogSearch, { variant: "pill", basePath: basePath, endpoint: searchEndpoint, className: "sm:w-80" })] }), _jsx("div", { className: "border-t border-border" }), posts.length === 0 ? (_jsx("p", { className: "py-16 text-center text-muted-foreground", children: query ? t("common.no_results_for_query", { query }) : t("common.no_posts_yet") })) : (_jsxs(_Fragment, { children: [featured && (_jsx("div", { className: "mt-8", children: _jsx(FeaturedPostCard, { post: featured, basePath: basePath }) })), gridPosts.length > 0 && (_jsx("div", { className: "mt-8 grid items-start sm:grid-cols-2 lg:grid-cols-3 gap-6", children: gridPosts.map((p, i) => (_jsx(EditorialPostCard, { post: p, index: i, basePath: basePath }, p.id))) }))] })), !query && totalPages > 1 && (_jsx("nav", { className: "mt-10 flex justify-center gap-2 text-sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (_jsx("a", { href: `${basePath}?page=${n}`, className: n === page
26
+ }), "\u00A0\u00B7\u00A0", _jsx("a", { href: basePath, className: "text-primary hover:underline", children: t("common.clear") })] }))] }), _jsx(BlogSearch, { variant: "pill", basePath: basePath, endpoint: searchEndpoint, className: "sm:w-80", locale: locale })] }), _jsx("div", { className: "border-t border-border" }), posts.length === 0 ? (_jsx("p", { className: "py-16 text-center text-muted-foreground", children: query ? t("common.no_results_for_query", { query }) : t("common.no_posts_yet") })) : (_jsxs(_Fragment, { children: [featured && (_jsx("div", { className: "mt-8", children: _jsx(FeaturedPostCard, { post: featured, basePath: basePath, locale: locale }) })), gridPosts.length > 0 && (_jsx("div", { className: "mt-8 grid items-start sm:grid-cols-2 lg:grid-cols-3 gap-6", children: gridPosts.map((p, i) => (_jsx(EditorialPostCard, { post: p, index: i, basePath: basePath, locale: locale }, p.id))) }))] })), !query && totalPages > 1 && (_jsx("nav", { className: "mt-10 flex justify-center gap-2 text-sm", children: Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (_jsx("a", { href: `${basePath}?page=${n}`, className: n === page
28
27
  ? "rounded-full bg-primary px-4 py-1.5 text-primary-foreground"
29
28
  : "rounded-full border border-border px-4 py-1.5 hover:bg-accent", children: n }, n))) }))] }));
30
29
  }
@@ -1,3 +1,3 @@
1
1
  import type { BlogIndexLayoutProps } from "./index.js";
2
- export declare function BlogIndexGrid({ posts, basePath, searchEndpoint, query, page, totalPages, title, }: BlogIndexLayoutProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function BlogIndexGrid({ posts, basePath, searchEndpoint, query, page, totalPages, title, locale, }: BlogIndexLayoutProps): import("react/jsx-runtime").JSX.Element;
3
3
  //# sourceMappingURL=blog-index-grid.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blog-index-grid.d.ts","sourceRoot":"","sources":["../../../src/components/layouts/blog-index-grid.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAMvD,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,KAAK,EACL,IAAI,EACJ,UAAU,EACV,KAAsC,GACvC,EAAE,oBAAoB,2CAoDtB"}
1
+ {"version":3,"file":"blog-index-grid.d.ts","sourceRoot":"","sources":["../../../src/components/layouts/blog-index-grid.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,KAAK,EACL,IAAI,EACJ,UAAU,EACV,KAAK,EACL,MAAa,GACd,EAAE,oBAAoB,2CAqDtB"}