hazo_blog 1.1.0 → 1.3.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.
- package/CHANGE_LOG.md +58 -0
- package/dist/components/admin/featured-image-field.d.ts.map +1 -1
- package/dist/components/admin/featured-image-field.js +9 -4
- package/dist/components/admin/post-form.d.ts.map +1 -1
- package/dist/components/admin/post-form.js +7 -4
- package/dist/components/author-bio.js +1 -1
- package/dist/components/blog-search.js +1 -1
- package/dist/components/faq-section.js +1 -1
- package/dist/components/featured-post-card.js +1 -1
- package/dist/components/layouts/blog-index-editorial.js +1 -1
- package/dist/components/layouts/blog-index-grid.js +1 -1
- package/dist/components/mdx/youtube.js +1 -1
- package/dist/components/post-card.js +1 -1
- package/dist/components/post-hero.js +1 -1
- package/dist/components/related-posts.js +1 -1
- package/dist/components/table-of-contents.js +1 -1
- package/dist/locales/en.json +69 -1
- package/dist/next/blog-content.js +1 -1
- package/dist/next/pages.js +1 -1
- package/dist/ui/blog-admin-panel.d.ts.map +1 -1
- package/dist/ui/blog-admin-panel.js +14 -3
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/layout-selector.d.ts.map +1 -1
- package/dist/ui/layout-selector.js +6 -3
- package/dist/ui/panel-dialog.d.ts +5 -1
- package/dist/ui/panel-dialog.d.ts.map +1 -1
- package/dist/ui/panel-dialog.js +7 -2
- package/dist/ui/post-list-table.d.ts.map +1 -1
- package/dist/ui/post-list-table.js +9 -6
- package/dist/ui/post-preview.d.ts +7 -0
- package/dist/ui/post-preview.d.ts.map +1 -0
- package/dist/ui/post-preview.js +13 -0
- package/package.json +15 -15
package/CHANGE_LOG.md
CHANGED
|
@@ -3,6 +3,64 @@
|
|
|
3
3
|
All notable changes are documented here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 1.3.0 — 2026-09-20
|
|
7
|
+
|
|
8
|
+
**Migrated onto the canonical `hazo_core/i18n` bridge**
|
|
9
|
+
|
|
10
|
+
`src/lib/create_package_t.ts` (this package's local ~120-line copy of the `createPackageT`
|
|
11
|
+
pattern) is deleted. All 13 call sites now import `createPackageT` from `hazo_core/i18n` — the
|
|
12
|
+
canonical, workspace-shared version of the same bridge (flat-key catalog, `{{var}}` mustache
|
|
13
|
+
interpolation, optional fire-and-forget registration into `hazo_i18n`'s cross-package registry
|
|
14
|
+
when that package is installed). No `resolvePackageLocale` callers existed in this package, so
|
|
15
|
+
none needed updating. `hazo_core` peer bumped to `^2.4.0` (first version shipping
|
|
16
|
+
`hazo_core/i18n`); `hazo_i18n` peer bumped to `^0.5.0`.
|
|
17
|
+
|
|
18
|
+
**Extracted ~68 residual hardcoded strings into `src/locales/en.json`**
|
|
19
|
+
|
|
20
|
+
The admin/editorial UI surface — `PostForm`, `FeaturedImageField`, `BlogAdminPanel`,
|
|
21
|
+
`PostListTable`, `LayoutSelector`, and `PostPreview` — had never been wired through `t()`. Every
|
|
22
|
+
label, placeholder, button, status message, and error string across those six components is now a
|
|
23
|
+
flat, literal-dotted key in `en.json` (`post_form.*`, `featured_image_field.*`,
|
|
24
|
+
`blog_admin_panel.*`, `post_list_table.*`, `layout_selector.*`, `post_preview.*`), routed through a
|
|
25
|
+
module-scope `t = createPackageT("hazo_blog", en)`. `en.json` now has 90 keys total, all
|
|
26
|
+
referenced, zero orphans (verified by a full-repo cross-reference of every `t("...")` call against
|
|
27
|
+
the catalog).
|
|
28
|
+
|
|
29
|
+
**New `/autotest` scenario group: `hazo_blog_i18n`**
|
|
30
|
+
|
|
31
|
+
`test-app/app/autotest/hazo_blog_i18n_scenarios.ts` — catalog loads (`t`/`t.has`/`t.raw` shape),
|
|
32
|
+
sampled keys resolve across both pre-existing and newly-extracted areas, `{{var}}` interpolation
|
|
33
|
+
substitutes correctly, and a coverage-threshold guard confirms every sampled key is a real
|
|
34
|
+
`en.json` entry. Wired into `test-app/app/autotest/page.tsx` alongside the existing
|
|
35
|
+
`hazo_blog_scenarios` group.
|
|
36
|
+
|
|
37
|
+
## 1.2.0 — 2026-09-12
|
|
38
|
+
|
|
39
|
+
**Side preview in the post editor (`PanelDialog` + `PostPreview`)**
|
|
40
|
+
|
|
41
|
+
The admin edit dialog now supports a split-pane layout showing the published article alongside the
|
|
42
|
+
editor. When editing a published post, a "Preview" toggle appears in the dialog header; clicking it
|
|
43
|
+
expands the dialog to `max-w-7xl` and loads the published article in an iframe on the right half.
|
|
44
|
+
|
|
45
|
+
**`PanelDialog`** gained two new optional props:
|
|
46
|
+
- `sideContent?: ReactNode` — when provided, the dialog widens and renders a two-column layout
|
|
47
|
+
(children on the left, sideContent on the right).
|
|
48
|
+
- `headerActions?: ReactNode` — rendered inline after the title, used by `BlogAdminPanel` for the
|
|
49
|
+
preview toggle button.
|
|
50
|
+
|
|
51
|
+
**`PostPreview`** — new component exported from `hazo_blog/ui`. A lightweight iframe wrapper that
|
|
52
|
+
takes a `url` prop (the post's public URL), shows a loading state, and provides an "Open in new
|
|
53
|
+
tab" link. Token-free Tailwind, same isolation rule as the rest of `src/ui/*`.
|
|
54
|
+
|
|
55
|
+
**`BlogAdminPanel`** wires it up automatically: when the `dialogTarget` is a published post, the
|
|
56
|
+
preview toggle and `PostPreview` are passed to `PanelDialog`. The iframe URL is constructed from
|
|
57
|
+
`blogBasePath` + slug (e.g. `/guides/my-post`), so consuming apps that set a custom `blogBasePath`
|
|
58
|
+
get correct preview URLs out of the box.
|
|
59
|
+
|
|
60
|
+
**Back-compat: zero break.** Both new `PanelDialog` props are optional; `PostPreview` is additive.
|
|
61
|
+
The dialog renders identically to before when `sideContent` is not provided (the only layout
|
|
62
|
+
difference is the title now sits in a border-bottom header row — visually near-identical).
|
|
63
|
+
|
|
6
64
|
## 1.1.0 — 2026-09-11
|
|
7
65
|
|
|
8
66
|
**`onPostSaved` config hook + `createBlogManageSlugRoutes` factory**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"featured-image-field.d.ts","sourceRoot":"","sources":["../../../src/components/admin/featured-image-field.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"featured-image-field.d.ts","sourceRoot":"","sources":["../../../src/components/admin/featured-image-field.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,uBAAuB;IACtC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAS,GACV,EAAE,uBAAuB,2CA+GzB"}
|
|
@@ -3,6 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useRef, useState } from "react";
|
|
4
4
|
import { cn } from "hazo_ui/utils";
|
|
5
5
|
import { ImageIcon, TrashIcon, UploadIcon } from "../icons.js";
|
|
6
|
+
import en from "../../locales/en.json" with { type: "json" };
|
|
7
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
8
|
+
const t = createPackageT("hazo_blog", en);
|
|
6
9
|
const inputCls = "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary";
|
|
7
10
|
export function FeaturedImageField({ value, onChange, uploader, className, }) {
|
|
8
11
|
const [uploading, setUploading] = useState(false);
|
|
@@ -20,7 +23,7 @@ export function FeaturedImageField({ value, onChange, uploader, className, }) {
|
|
|
20
23
|
onChange(url);
|
|
21
24
|
}
|
|
22
25
|
else {
|
|
23
|
-
setError("
|
|
26
|
+
setError(t("featured_image_field.upload_failed"));
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
finally {
|
|
@@ -34,13 +37,15 @@ export function FeaturedImageField({ value, onChange, uploader, className, }) {
|
|
|
34
37
|
return;
|
|
35
38
|
void handleFile(e.dataTransfer.files?.[0]);
|
|
36
39
|
}
|
|
37
|
-
return (_jsxs("div", { className: cn("space-y-2", className), children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: "
|
|
40
|
+
return (_jsxs("div", { className: cn("space-y-2", className), children: [_jsx("label", { className: "mb-1 block text-sm font-medium", children: t("featured_image_field.label") }), value ? (_jsxs("div", { className: "flex items-start gap-3", children: [_jsx("img", { src: value, alt: t("featured_image_field.alt_text"), className: "h-24 w-24 rounded-lg border border-border object-cover" }), _jsxs("button", { type: "button", onClick: () => onChange(""), className: "inline-flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium hover:bg-accent", children: [_jsx(TrashIcon, { width: 16, height: 16 }), t("featured_image_field.clear")] })] })) : (_jsxs("div", { onDragOver: (e) => {
|
|
38
41
|
if (!uploader)
|
|
39
42
|
return;
|
|
40
43
|
e.preventDefault();
|
|
41
44
|
setDragOver(true);
|
|
42
|
-
}, onDragLeave: () => setDragOver(false), onDrop: onDrop, className: cn("flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-muted px-4 py-8 text-center", dragOver && "border-primary bg-accent"), children: [_jsx(ImageIcon, { width: 28, height: 28, className: "text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: uploader
|
|
45
|
+
}, onDragLeave: () => setDragOver(false), onDrop: onDrop, className: cn("flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-muted px-4 py-8 text-center", dragOver && "border-primary bg-accent"), children: [_jsx(ImageIcon, { width: 28, height: 28, className: "text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: uploader
|
|
46
|
+
? t("featured_image_field.drag_drop_or")
|
|
47
|
+
: t("featured_image_field.no_image_set") }), uploader && (_jsxs("button", { type: "button", disabled: uploading, onClick: () => fileInputRef.current?.click(), className: "inline-flex items-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium hover:bg-accent disabled:opacity-50", children: [_jsx(UploadIcon, { width: 16, height: 16 }), uploading ? t("featured_image_field.uploading") : t("featured_image_field.upload")] }))] })), uploader && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => {
|
|
43
48
|
void handleFile(e.target.files?.[0]);
|
|
44
49
|
e.target.value = "";
|
|
45
|
-
} })), error && _jsx("p", { className: "text-sm text-destructive", children: error }), _jsx("input", { className: inputCls, value: value, onChange: (e) => onChange(e.target.value), placeholder: "
|
|
50
|
+
} })), error && _jsx("p", { className: "text-sm text-destructive", children: error }), _jsx("input", { className: inputCls, value: value, onChange: (e) => onChange(e.target.value), placeholder: t("featured_image_field.url_placeholder") })] }));
|
|
46
51
|
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -6,6 +6,9 @@ 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);
|
|
9
12
|
const DEFAULT_EMBEDS = [
|
|
10
13
|
{ name: "youtube", label: "YT", snippet: '\n<YouTube id="VIDEO_ID" title="" />\n' },
|
|
11
14
|
{
|
|
@@ -73,21 +76,21 @@ export function PostForm({ post, categories = [], endpoint = "/api/admin/blog",
|
|
|
73
76
|
body: JSON.stringify(payload),
|
|
74
77
|
});
|
|
75
78
|
if (!res.ok) {
|
|
76
|
-
setError(
|
|
79
|
+
setError(t("post_form.save_failed_with_status", { status: res.status }));
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
82
|
onSaved?.(await res.json());
|
|
80
83
|
}
|
|
81
84
|
catch {
|
|
82
|
-
setError("
|
|
85
|
+
setError(t("post_form.save_failed"));
|
|
83
86
|
}
|
|
84
87
|
finally {
|
|
85
88
|
setSaving(false);
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
const inputCls = "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary";
|
|
89
|
-
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: "
|
|
92
|
+
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) => {
|
|
90
93
|
setSlugTouched(true);
|
|
91
94
|
setSlug(e.target.value);
|
|
92
|
-
}, placeholder: "
|
|
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") })] })] }));
|
|
93
96
|
}
|
|
@@ -2,7 +2,7 @@ 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
4
|
import en from "../locales/en.json" with { type: "json" };
|
|
5
|
-
import { createPackageT } from "
|
|
5
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
6
6
|
const t = createPackageT("hazo_blog", en);
|
|
7
7
|
export function AuthorBio({ author, className }) {
|
|
8
8
|
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 })] })] }));
|
|
@@ -7,7 +7,7 @@ import { cn } from "hazo_ui/utils";
|
|
|
7
7
|
import { trackBlogEvent } from "../lib/analytics.js";
|
|
8
8
|
import { SearchIcon } from "./icons.js";
|
|
9
9
|
import en from "../locales/en.json" with { type: "json" };
|
|
10
|
-
import { createPackageT } from "
|
|
10
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
11
11
|
const t = createPackageT("hazo_blog", en);
|
|
12
12
|
export function BlogSearch({ endpoint, basePath = "/blog", placeholder = t("blog_search.placeholder"), className, variant = "default", }) {
|
|
13
13
|
const url = endpoint ?? `${basePath}/api/search`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "hazo_ui/utils";
|
|
3
3
|
import en from "../locales/en.json" with { type: "json" };
|
|
4
|
-
import { createPackageT } from "
|
|
4
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
5
5
|
const t = createPackageT("hazo_blog", en);
|
|
6
6
|
/** Renders FAQ as a no-JS <details> accordion (the FAQPage JSON-LD is emitted separately). */
|
|
7
7
|
export function FaqSection({ faq, title = t("faq_section.title"), className }) {
|
|
@@ -8,7 +8,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
10
|
import en from "../locales/en.json" with { type: "json" };
|
|
11
|
-
import { createPackageT } from "
|
|
11
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
12
12
|
const t = createPackageT("hazo_blog", en);
|
|
13
13
|
export function FeaturedPostCard({ post, basePath = "/blog", className }) {
|
|
14
14
|
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: {
|
|
@@ -14,7 +14,7 @@ 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
16
|
import en from "../../locales/en.json" with { type: "json" };
|
|
17
|
-
import { createPackageT } from "
|
|
17
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
18
18
|
const t = createPackageT("hazo_blog", en);
|
|
19
19
|
export function BlogIndexEditorial({ posts, basePath, searchEndpoint, query, page, totalPages, title = t("common.default_blog_title"), description, }) {
|
|
20
20
|
const showFeatured = !query && posts.length > 0;
|
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
import { PostCard } from "../post-card.js";
|
|
11
11
|
import { BlogSearch } from "../blog-search.js";
|
|
12
12
|
import en from "../../locales/en.json" with { type: "json" };
|
|
13
|
-
import { createPackageT } from "
|
|
13
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
14
14
|
const t = createPackageT("hazo_blog", en);
|
|
15
15
|
export function BlogIndexGrid({ posts, basePath, searchEndpoint, query, page, totalPages, title = t("common.default_blog_title"), }) {
|
|
16
16
|
return (_jsxs("div", { className: "mx-auto max-w-6xl px-4 py-10", children: [_jsxs("div", { className: "mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold text-foreground", children: title }), query && (_jsxs("p", { className: "mt-1 text-sm text-muted-foreground", children: [t("common.results_count", {
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
// <YouTube id="..." title="..." start={30} /> — privacy-friendly lazy embed.
|
|
3
3
|
import { cn } from "hazo_ui/utils";
|
|
4
4
|
import en from "../../locales/en.json" with { type: "json" };
|
|
5
|
-
import { createPackageT } from "
|
|
5
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
6
6
|
const t = createPackageT("hazo_blog", en);
|
|
7
7
|
export function YouTube({ id, title = t("youtube.default_title"), start, className }) {
|
|
8
8
|
const src = `https://www.youtube-nocookie.com/embed/${id}${start ? `?start=${start}` : ""}`;
|
|
@@ -3,7 +3,7 @@ import Link from "next/link";
|
|
|
3
3
|
import Image from "next/image";
|
|
4
4
|
import { cn } from "hazo_ui/utils";
|
|
5
5
|
import en from "../locales/en.json" with { type: "json" };
|
|
6
|
-
import { createPackageT } from "
|
|
6
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
7
7
|
const t = createPackageT("hazo_blog", en);
|
|
8
8
|
export function PostCard({ post, basePath = "/blog", className }) {
|
|
9
9
|
return (_jsx("article", { className: cn("group overflow-hidden rounded-xl border border-border bg-card transition-shadow hover:shadow-md", className), children: _jsxs(Link, { href: `${basePath}/${post.slug}`, className: "block", children: [post.featured_image && (_jsx("div", { className: "relative aspect-video w-full overflow-hidden", children: _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" }) })), _jsxs("div", { className: "p-5", children: [post.category && (_jsx("span", { className: "mb-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-medium", style: {
|
|
@@ -2,7 +2,7 @@ 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
4
|
import en from "../locales/en.json" with { type: "json" };
|
|
5
|
-
import { createPackageT } from "
|
|
5
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
6
6
|
const t = createPackageT("hazo_blog", en);
|
|
7
7
|
function formatDate(value) {
|
|
8
8
|
if (!value)
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cn } from "hazo_ui/utils";
|
|
3
3
|
import { PostCard } from "./post-card.js";
|
|
4
4
|
import en from "../locales/en.json" with { type: "json" };
|
|
5
|
-
import { createPackageT } from "
|
|
5
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
6
6
|
const t = createPackageT("hazo_blog", en);
|
|
7
7
|
export function RelatedPosts({ posts, basePath = "/blog", title = t("related_posts.title"), className, }) {
|
|
8
8
|
if (!posts || posts.length === 0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "hazo_ui/utils";
|
|
3
3
|
import en from "../locales/en.json" with { type: "json" };
|
|
4
|
-
import { createPackageT } from "
|
|
4
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
5
5
|
const t = createPackageT("hazo_blog", en);
|
|
6
6
|
export function TableOfContents({ headings, title = t("table_of_contents.title"), className }) {
|
|
7
7
|
if (!headings || headings.length === 0)
|
package/dist/locales/en.json
CHANGED
|
@@ -20,5 +20,73 @@
|
|
|
20
20
|
"index_page.meta_description": "Articles and guides from {{siteName}}.",
|
|
21
21
|
"tag_page.heading_prefix": "Tagged:",
|
|
22
22
|
"tag_page.no_posts": "No posts with this tag.",
|
|
23
|
-
"tag_page.meta_title": "{{tag}} | {{siteName}} Blog"
|
|
23
|
+
"tag_page.meta_title": "{{tag}} | {{siteName}} Blog",
|
|
24
|
+
"post_form.title_label": "Title",
|
|
25
|
+
"post_form.title_placeholder": "Post title",
|
|
26
|
+
"post_form.slug_label": "Slug",
|
|
27
|
+
"post_form.slug_placeholder": "auto-generated-from-title",
|
|
28
|
+
"post_form.content_label": "Content (MDX)",
|
|
29
|
+
"post_form.content_placeholder": "Write your post in Markdown / MDX…",
|
|
30
|
+
"post_form.category_label": "Category",
|
|
31
|
+
"post_form.category_none_option": "— none —",
|
|
32
|
+
"post_form.tags_label": "Tags (comma-separated)",
|
|
33
|
+
"post_form.tags_placeholder": "guides, timers",
|
|
34
|
+
"post_form.locale_label": "Locale",
|
|
35
|
+
"post_form.locale_placeholder": "e.g. fr, pt-BR (blank = default locale)",
|
|
36
|
+
"post_form.translation_of_label": "Translation of (post ID)",
|
|
37
|
+
"post_form.translation_of_placeholder": "Leave blank for the root post",
|
|
38
|
+
"post_form.indexable_label": "Indexable (include in sitemap/robots)",
|
|
39
|
+
"post_form.meta_title_label": "Meta title (SEO)",
|
|
40
|
+
"post_form.meta_description_label": "Meta description (SEO)",
|
|
41
|
+
"post_form.faq_legend": "FAQ",
|
|
42
|
+
"post_form.faq_question_placeholder": "Question",
|
|
43
|
+
"post_form.faq_answer_placeholder": "Answer",
|
|
44
|
+
"post_form.faq_add_item": "+ Add FAQ item",
|
|
45
|
+
"post_form.save_failed_with_status": "Save failed ({{status}})",
|
|
46
|
+
"post_form.save_failed": "Save failed",
|
|
47
|
+
"post_form.cancel": "Cancel",
|
|
48
|
+
"post_form.save_draft": "Save draft",
|
|
49
|
+
"post_form.saving": "Saving…",
|
|
50
|
+
"post_form.publish": "Publish",
|
|
51
|
+
"featured_image_field.label": "Featured image",
|
|
52
|
+
"featured_image_field.alt_text": "Featured",
|
|
53
|
+
"featured_image_field.clear": "Clear",
|
|
54
|
+
"featured_image_field.upload_failed": "Upload failed — try again or paste a URL below.",
|
|
55
|
+
"featured_image_field.drag_drop_or": "Drag & drop an image, or",
|
|
56
|
+
"featured_image_field.no_image_set": "No image set — paste a URL below",
|
|
57
|
+
"featured_image_field.upload": "Upload",
|
|
58
|
+
"featured_image_field.uploading": "Uploading…",
|
|
59
|
+
"featured_image_field.url_placeholder": "https://…",
|
|
60
|
+
"blog_admin_panel.posts_heading": "Posts",
|
|
61
|
+
"blog_admin_panel.new_post_button": "New Post",
|
|
62
|
+
"blog_admin_panel.error_prefix": "Error: {{message}}",
|
|
63
|
+
"blog_admin_panel.loading_posts": "Loading posts…",
|
|
64
|
+
"blog_admin_panel.index_layout_heading": "Index Layout",
|
|
65
|
+
"blog_admin_panel.edit_post_title": "Edit Post",
|
|
66
|
+
"blog_admin_panel.new_post_title": "New Post",
|
|
67
|
+
"blog_admin_panel.hide_preview": "Hide preview",
|
|
68
|
+
"blog_admin_panel.preview": "Preview",
|
|
69
|
+
"blog_admin_panel.delete_confirm": "Delete \"{{title}}\"? This cannot be undone.",
|
|
70
|
+
"blog_admin_panel.delete_failed": "Delete failed",
|
|
71
|
+
"post_list_table.search_placeholder": "Search title or slug…",
|
|
72
|
+
"post_list_table.filter_all": "All",
|
|
73
|
+
"post_list_table.filter_draft": "Draft",
|
|
74
|
+
"post_list_table.filter_published": "Published",
|
|
75
|
+
"post_list_table.filter_scheduled": "Scheduled",
|
|
76
|
+
"post_list_table.col_title": "Title",
|
|
77
|
+
"post_list_table.col_status": "Status",
|
|
78
|
+
"post_list_table.col_category": "Category",
|
|
79
|
+
"post_list_table.col_published": "Published",
|
|
80
|
+
"post_list_table.no_posts_found": "No posts found.",
|
|
81
|
+
"post_list_table.untitled": "(untitled)",
|
|
82
|
+
"post_list_table.edit": "Edit",
|
|
83
|
+
"post_list_table.delete": "Delete",
|
|
84
|
+
"layout_selector.not_persisted_notice": "Settings table not migrated — layout is read-only.",
|
|
85
|
+
"layout_selector.save_failed": "Failed to save layout",
|
|
86
|
+
"layout_selector.saving": "Saving…",
|
|
87
|
+
"layout_selector.active": "Active",
|
|
88
|
+
"post_preview.heading": "Published preview",
|
|
89
|
+
"post_preview.open_in_new_tab": "Open in new tab",
|
|
90
|
+
"post_preview.loading": "Loading preview…",
|
|
91
|
+
"post_preview.iframe_title": "Post preview"
|
|
24
92
|
}
|
|
@@ -8,7 +8,7 @@ import remarkGfm from "remark-gfm";
|
|
|
8
8
|
import { defaultMdxComponents } from "../components/mdx/index.js";
|
|
9
9
|
import { sanitizeMdx } from "../lib/text.js";
|
|
10
10
|
import en from "../locales/en.json" with { type: "json" };
|
|
11
|
-
import { createPackageT } from "
|
|
11
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
12
12
|
const t = createPackageT("hazo_blog", en);
|
|
13
13
|
// MDXRemote is an async RSC but TypeScript surfaces it as ComponentType via
|
|
14
14
|
// React 18 JSX types. Cast to the actual async-function signature so we can
|
package/dist/next/pages.js
CHANGED
|
@@ -27,7 +27,7 @@ import { BlogContent } from "./blog-content.js";
|
|
|
27
27
|
import { BLOG_INDEX_LAYOUTS } from "../components/layouts/index.js";
|
|
28
28
|
import { formatPostDate } from "../lib/dates.js";
|
|
29
29
|
import en from "../locales/en.json" with { type: "json" };
|
|
30
|
-
import { createPackageT } from "
|
|
30
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
31
31
|
const t = createPackageT("hazo_blog", en);
|
|
32
32
|
function JsonLd({ data }) {
|
|
33
33
|
// Escape `<` (and the line/para separators) so author-supplied strings can't
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-admin-panel.d.ts","sourceRoot":"","sources":["../../src/ui/blog-admin-panel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blog-admin-panel.d.ts","sourceRoot":"","sources":["../../src/ui/blog-admin-panel.tsx"],"names":[],"mappings":"AAwBA,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,QAAQ,EACR,YAAsB,EACtB,YAAY,EACZ,eAAe,EACf,SAAS,GACV,EAAE,mBAAmB,2CAiIrB"}
|
|
@@ -13,10 +13,15 @@ import { PostForm } from "../components/admin/post-form.js";
|
|
|
13
13
|
import { PostListTable } from "./post-list-table.js";
|
|
14
14
|
import { LayoutSelector } from "./layout-selector.js";
|
|
15
15
|
import { PanelDialog } from "./panel-dialog.js";
|
|
16
|
+
import { PostPreview } from "./post-preview.js";
|
|
16
17
|
import { useBlogAdmin } from "./use-blog-admin.js";
|
|
18
|
+
import en from "../locales/en.json" with { type: "json" };
|
|
19
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
20
|
+
const t = createPackageT("hazo_blog", en);
|
|
17
21
|
export function BlogAdminPanel({ fetchFn, basePath, blogBasePath = "/blog", settingsPath, imageUploadPath, className, }) {
|
|
18
22
|
const admin = useBlogAdmin({ fetchFn, basePath, settingsPath, imageUploadPath });
|
|
19
23
|
const [dialogTarget, setDialogTarget] = useState(undefined);
|
|
24
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
20
25
|
const dialogOpen = dialogTarget !== undefined;
|
|
21
26
|
const resolvedImageUploadPath = imageUploadPath ?? `${basePath}/images`;
|
|
22
27
|
function openCreate() {
|
|
@@ -27,21 +32,27 @@ export function BlogAdminPanel({ fetchFn, basePath, blogBasePath = "/blog", sett
|
|
|
27
32
|
}
|
|
28
33
|
function closeDialog() {
|
|
29
34
|
setDialogTarget(undefined);
|
|
35
|
+
setShowPreview(false);
|
|
30
36
|
}
|
|
31
37
|
async function handleDelete(post) {
|
|
32
|
-
if (typeof window !== "undefined" &&
|
|
38
|
+
if (typeof window !== "undefined" &&
|
|
39
|
+
!window.confirm(t("blog_admin_panel.delete_confirm", { title: post.title || post.slug }))) {
|
|
33
40
|
return;
|
|
34
41
|
}
|
|
35
42
|
try {
|
|
36
43
|
await admin.deletePost(post.id);
|
|
37
44
|
}
|
|
38
45
|
catch (err) {
|
|
39
|
-
const message = err instanceof Error ? err.message : "
|
|
46
|
+
const message = err instanceof Error ? err.message : t("blog_admin_panel.delete_failed");
|
|
40
47
|
if (typeof window !== "undefined")
|
|
41
48
|
window.alert(message);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
|
-
return (_jsxs("div", { className: `space-y-8 text-zinc-900 dark:text-zinc-100 ${className ?? ""}`, children: [_jsxs("section", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold", children: "
|
|
51
|
+
return (_jsxs("div", { className: `space-y-8 text-zinc-900 dark:text-zinc-100 ${className ?? ""}`, children: [_jsxs("section", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold", children: t("blog_admin_panel.posts_heading") }), _jsx("button", { type: "button", onClick: openCreate, className: "rounded-lg bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:opacity-90 dark:bg-zinc-100 dark:text-zinc-900", children: t("blog_admin_panel.new_post_button") })] }), admin.postsError && (_jsx("p", { className: "text-sm text-red-600 dark:text-red-400", children: t("blog_admin_panel.error_prefix", { message: admin.postsError }) })), admin.postsLoading && admin.posts.length === 0 ? (_jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: t("blog_admin_panel.loading_posts") })) : (_jsx(PostListTable, { posts: admin.posts, blogBasePath: blogBasePath, onEdit: openEdit, onDelete: handleDelete }))] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-lg font-semibold", children: t("blog_admin_panel.index_layout_heading") }), admin.settingsError && (_jsx("p", { className: "mb-2 text-sm text-red-600 dark:text-red-400", children: t("blog_admin_panel.error_prefix", { message: admin.settingsError }) })), admin.settingsState && (_jsx(LayoutSelector, { value: admin.settingsState.settings.indexLayout, persisted: admin.settingsState.persisted, onSelect: (next) => admin.updateSettings({ indexLayout: next }) }))] }), _jsx(PanelDialog, { open: dialogOpen, onClose: closeDialog, title: dialogTarget
|
|
52
|
+
? t("blog_admin_panel.edit_post_title")
|
|
53
|
+
: t("blog_admin_panel.new_post_title"), headerActions: dialogTarget?.status === "published" ? (_jsx("button", { type: "button", onClick: () => setShowPreview((p) => !p), className: `rounded-md border px-3 py-1 text-xs font-medium transition-colors ${showPreview
|
|
54
|
+
? "border-zinc-900 bg-zinc-900 text-white dark:border-zinc-100 dark:bg-zinc-100 dark:text-zinc-900"
|
|
55
|
+
: "border-zinc-200 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"}`, children: showPreview ? t("blog_admin_panel.hide_preview") : t("blog_admin_panel.preview") })) : undefined, sideContent: showPreview && dialogTarget?.slug ? (_jsx(PostPreview, { url: `${blogBasePath.replace(/\/$/, "")}/${dialogTarget.slug}` })) : undefined, children: _jsx(PostForm, { post: dialogTarget ?? null, categories: admin.categories, endpoint: basePath, imageUploadEndpoint: resolvedImageUploadPath, fetchFn: fetchFn, onCancel: closeDialog, onSaved: () => {
|
|
45
56
|
closeDialog();
|
|
46
57
|
void admin.refetchPosts();
|
|
47
58
|
} }) })] }));
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { BlogAdminPanel, type BlogAdminPanelProps } from "./blog-admin-panel.js"
|
|
|
2
2
|
export { PostListTable, type PostListTableProps } from "./post-list-table.js";
|
|
3
3
|
export { LayoutSelector, type LayoutSelectorProps } from "./layout-selector.js";
|
|
4
4
|
export { PanelDialog, type PanelDialogProps } from "./panel-dialog.js";
|
|
5
|
+
export { PostPreview, type PostPreviewProps } from "./post-preview.js";
|
|
5
6
|
export { useBlogAdmin, type UseBlogAdminOptions, type UseBlogAdminResult, type BlogSettingsState, } from "./use-blog-admin.js";
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/ui/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC"}
|
package/dist/ui/index.js
CHANGED
|
@@ -6,4 +6,5 @@ export { BlogAdminPanel } from "./blog-admin-panel.js";
|
|
|
6
6
|
export { PostListTable } from "./post-list-table.js";
|
|
7
7
|
export { LayoutSelector } from "./layout-selector.js";
|
|
8
8
|
export { PanelDialog } from "./panel-dialog.js";
|
|
9
|
+
export { PostPreview } from "./post-preview.js";
|
|
9
10
|
export { useBlogAdmin, } from "./use-blog-admin.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout-selector.d.ts","sourceRoot":"","sources":["../../src/ui/layout-selector.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAiD,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"layout-selector.d.ts","sourceRoot":"","sources":["../../src/ui/layout-selector.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAiD,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMxG,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,wFAAwF;IACxF,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,mBAAmB,2CAmE5F"}
|
|
@@ -8,6 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// and a banner explains why (an update would just 503).
|
|
9
9
|
import { useEffect, useState } from "react";
|
|
10
10
|
import { BLOG_INDEX_LAYOUT_META, BLOG_INDEX_LAYOUT_IDS } from "../lib/layouts.js";
|
|
11
|
+
import en from "../locales/en.json" with { type: "json" };
|
|
12
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
13
|
+
const t = createPackageT("hazo_blog", en);
|
|
11
14
|
export function LayoutSelector({ value, persisted, onSelect, className }) {
|
|
12
15
|
const [current, setCurrent] = useState(value);
|
|
13
16
|
const [saving, setSaving] = useState(null);
|
|
@@ -28,17 +31,17 @@ export function LayoutSelector({ value, persisted, onSelect, className }) {
|
|
|
28
31
|
}
|
|
29
32
|
catch (err) {
|
|
30
33
|
setCurrent(previous); // revert
|
|
31
|
-
setError(err instanceof Error ? err.message : "
|
|
34
|
+
setError(err instanceof Error ? err.message : t("layout_selector.save_failed"));
|
|
32
35
|
}
|
|
33
36
|
finally {
|
|
34
37
|
setSaving(null);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
return (_jsxs("div", { className: className, children: [!persisted && (_jsx("p", { className: "mb-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-300", children: "
|
|
40
|
+
return (_jsxs("div", { className: className, children: [!persisted && (_jsx("p", { className: "mb-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-300", children: t("layout_selector.not_persisted_notice") })), error && _jsx("p", { className: "mb-3 text-sm text-red-600 dark:text-red-400", children: error }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: BLOG_INDEX_LAYOUT_IDS.map((id) => {
|
|
38
41
|
const meta = BLOG_INDEX_LAYOUT_META[id];
|
|
39
42
|
const selected = current === id;
|
|
40
43
|
return (_jsxs("button", { type: "button", disabled: !persisted || saving !== null, onClick: () => void handleSelect(id), className: `rounded-lg border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${selected
|
|
41
44
|
? "border-zinc-900 bg-zinc-50 dark:border-zinc-100 dark:bg-zinc-800"
|
|
42
|
-
: "border-zinc-200 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-900"}`, "aria-pressed": selected, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: meta.label }), selected && (_jsx("span", { className: "rounded-full bg-zinc-900 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white dark:bg-zinc-100 dark:text-zinc-900", children: saving === id ? "
|
|
45
|
+
: "border-zinc-200 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-900"}`, "aria-pressed": selected, children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: meta.label }), selected && (_jsx("span", { className: "rounded-full bg-zinc-900 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white dark:bg-zinc-100 dark:text-zinc-900", children: saving === id ? t("layout_selector.saving") : t("layout_selector.active") }))] }), _jsx("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-400", children: meta.description })] }, id));
|
|
43
46
|
}) })] }));
|
|
44
47
|
}
|
|
@@ -4,7 +4,11 @@ export interface PanelDialogProps {
|
|
|
4
4
|
onClose: () => void;
|
|
5
5
|
title?: string;
|
|
6
6
|
children: ReactNode;
|
|
7
|
+
/** When provided, dialog expands to a two-column layout with this on the right. */
|
|
8
|
+
sideContent?: ReactNode;
|
|
9
|
+
/** Rendered inline after the title (e.g. a toggle button). */
|
|
10
|
+
headerActions?: ReactNode;
|
|
7
11
|
className?: string;
|
|
8
12
|
}
|
|
9
|
-
export declare function PanelDialog({ open, onClose, title, children, className }: PanelDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export declare function PanelDialog({ open, onClose, title, children, sideContent, headerActions, className }: PanelDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
10
14
|
//# sourceMappingURL=panel-dialog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel-dialog.d.ts","sourceRoot":"","sources":["../../src/ui/panel-dialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"panel-dialog.d.ts","sourceRoot":"","sources":["../../src/ui/panel-dialog.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;IACpB,mFAAmF;IACnF,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,EAAE,gBAAgB,kDAkDtH"}
|
package/dist/ui/panel-dialog.js
CHANGED
|
@@ -4,8 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// Deliberately NOT hazo_ui's Dialog (token-dependent — see the isolation
|
|
5
5
|
// rule for src/ui/*). A plain fixed-position overlay + centered panel is
|
|
6
6
|
// enough for an admin-panel context; no portal library needed.
|
|
7
|
+
//
|
|
8
|
+
// `sideContent` enables a split-pane layout: children on the left,
|
|
9
|
+
// sideContent on the right, dialog widens to max-w-7xl. Used by
|
|
10
|
+
// BlogAdminPanel to show a live article preview alongside the editor.
|
|
7
11
|
import { useEffect } from "react";
|
|
8
|
-
export function PanelDialog({ open, onClose, title, children, className }) {
|
|
12
|
+
export function PanelDialog({ open, onClose, title, children, sideContent, headerActions, className }) {
|
|
9
13
|
useEffect(() => {
|
|
10
14
|
if (!open)
|
|
11
15
|
return;
|
|
@@ -18,5 +22,6 @@ export function PanelDialog({ open, onClose, title, children, className }) {
|
|
|
18
22
|
}, [open, onClose]);
|
|
19
23
|
if (!open)
|
|
20
24
|
return null;
|
|
21
|
-
|
|
25
|
+
const wide = !!sideContent;
|
|
26
|
+
return (_jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [_jsx("div", { className: "fixed inset-0 bg-black/50", onClick: onClose, "aria-hidden": "true" }), _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": title, className: `relative z-10 max-h-[90vh] w-full rounded-lg border border-zinc-200 bg-white shadow-xl dark:border-zinc-800 dark:bg-zinc-900 ${wide ? "max-w-7xl" : "max-w-2xl"} ${className ?? ""}`, children: [(title || headerActions) && (_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-100 px-6 py-4 dark:border-zinc-800", children: [title && (_jsx("h2", { className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: title })), headerActions && _jsx("div", { className: "flex items-center gap-2", children: headerActions })] })), wide ? (_jsxs("div", { className: "flex max-h-[calc(90vh-60px)] min-h-0", children: [_jsx("div", { className: "w-1/2 shrink-0 overflow-y-auto border-r border-zinc-100 p-6 dark:border-zinc-800", children: children }), _jsx("div", { className: "flex w-1/2 min-w-0 flex-col", children: sideContent })] })) : (_jsx("div", { className: "max-h-[calc(90vh-60px)] overflow-y-auto p-6", children: children }))] })] }));
|
|
22
27
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-list-table.d.ts","sourceRoot":"","sources":["../../src/ui/post-list-table.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"post-list-table.d.ts","sourceRoot":"","sources":["../../src/ui/post-list-table.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,mBAAmB,CAAC;AAO3E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC/B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2BD,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,YAAsB,EACtB,MAAM,EACN,QAAQ,EACR,SAAS,GACV,EAAE,kBAAkB,2CAmHpB"}
|
|
@@ -10,11 +10,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
// hazo_theme's CSS variables.
|
|
11
11
|
import { useMemo, useState } from "react";
|
|
12
12
|
import { formatPostDate } from "../lib/dates.js";
|
|
13
|
+
import en from "../locales/en.json" with { type: "json" };
|
|
14
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
15
|
+
const t = createPackageT("hazo_blog", en);
|
|
13
16
|
const STATUS_FILTERS = [
|
|
14
|
-
{ label: "
|
|
15
|
-
{ label: "
|
|
16
|
-
{ label: "
|
|
17
|
-
{ label: "
|
|
17
|
+
{ label: t("post_list_table.filter_all"), value: "all" },
|
|
18
|
+
{ label: t("post_list_table.filter_draft"), value: "draft" },
|
|
19
|
+
{ label: t("post_list_table.filter_published"), value: "published" },
|
|
20
|
+
{ label: t("post_list_table.filter_scheduled"), value: "scheduled" },
|
|
18
21
|
];
|
|
19
22
|
const STATUS_PILL_CLASS = {
|
|
20
23
|
draft: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300",
|
|
@@ -37,7 +40,7 @@ export function PostListTable({ posts, blogBasePath = "/blog", onEdit, onDelete,
|
|
|
37
40
|
return post.title.toLowerCase().includes(q) || post.slug.toLowerCase().includes(q);
|
|
38
41
|
});
|
|
39
42
|
}, [posts, query, statusFilter]);
|
|
40
|
-
return (_jsxs("div", { className: `space-y-3 ${className ?? ""}`, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "
|
|
43
|
+
return (_jsxs("div", { className: `space-y-3 ${className ?? ""}`, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: t("post_list_table.search_placeholder"), className: "w-full max-w-xs rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-sm text-zinc-900 outline-none focus:ring-2 focus:ring-zinc-400 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100" }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: STATUS_FILTERS.map((f) => (_jsx("button", { type: "button", onClick: () => setStatusFilter(f.value), className: `rounded-full border px-3 py-1 text-xs font-medium transition-colors ${statusFilter === f.value
|
|
41
44
|
? "border-zinc-900 bg-zinc-900 text-white dark:border-zinc-100 dark:bg-zinc-100 dark:text-zinc-900"
|
|
42
|
-
: "border-zinc-200 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-800"}`, children: f.label }, f.value))) })] }), _jsx("div", { className: "overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-800", children: _jsxs("table", { className: "w-full min-w-[720px] border-collapse text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "bg-zinc-50 dark:bg-zinc-900", children: [_jsx("th", { className: "border-b border-zinc-200 px-3 py-2 text-left font-semibold text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: "
|
|
45
|
+
: "border-zinc-200 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-800"}`, children: f.label }, f.value))) })] }), _jsx("div", { className: "overflow-x-auto rounded-lg border border-zinc-200 dark:border-zinc-800", children: _jsxs("table", { className: "w-full min-w-[720px] border-collapse text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "bg-zinc-50 dark:bg-zinc-900", children: [_jsx("th", { className: "border-b border-zinc-200 px-3 py-2 text-left font-semibold text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: t("post_list_table.col_title") }), _jsx("th", { className: "border-b border-zinc-200 px-3 py-2 text-left font-semibold text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: t("post_list_table.col_status") }), _jsx("th", { className: "border-b border-zinc-200 px-3 py-2 text-left font-semibold text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: t("post_list_table.col_category") }), _jsx("th", { className: "border-b border-zinc-200 px-3 py-2 text-left font-semibold text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: t("post_list_table.col_published") }), _jsx("th", { className: "border-b border-zinc-200 px-3 py-2 text-right font-semibold text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: "\u00A0" })] }) }), _jsx("tbody", { children: filtered.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: 5, className: "px-3 py-8 text-center text-zinc-400", children: t("post_list_table.no_posts_found") }) })) : (filtered.map((post) => (_jsxs("tr", { className: "odd:bg-white even:bg-zinc-50 dark:odd:bg-zinc-950 dark:even:bg-zinc-900/50", children: [_jsx("td", { className: "max-w-xs truncate border-b border-zinc-100 px-3 py-2 dark:border-zinc-800", children: _jsx("a", { href: `${blogBasePath.replace(/\/$/, "")}/${post.slug}`, target: "_blank", rel: "noopener noreferrer", className: "font-medium text-zinc-900 hover:underline dark:text-zinc-100", children: post.title || t("post_list_table.untitled") }) }), _jsx("td", { className: "border-b border-zinc-100 px-3 py-2 dark:border-zinc-800", children: _jsx(StatusPill, { status: post.status }) }), _jsx("td", { className: "border-b border-zinc-100 px-3 py-2 text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: post.category?.name ?? "—" }), _jsx("td", { className: "whitespace-nowrap border-b border-zinc-100 px-3 py-2 text-zinc-600 dark:border-zinc-800 dark:text-zinc-300", children: formatPostDate(post.publish_date) || "—" }), _jsxs("td", { className: "whitespace-nowrap border-b border-zinc-100 px-3 py-2 text-right dark:border-zinc-800", children: [_jsx("button", { type: "button", onClick: () => onEdit(post), className: "mr-2 rounded-md border border-zinc-200 px-2.5 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800", children: t("post_list_table.edit") }), _jsx("button", { type: "button", onClick: () => void onDelete(post), className: "rounded-md border border-red-200 px-2.5 py-1 text-xs font-medium text-red-600 hover:bg-red-50 dark:border-red-900 dark:text-red-400 dark:hover:bg-red-950", children: t("post_list_table.delete") })] })] }, post.id)))) })] }) })] }));
|
|
43
46
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PostPreviewProps {
|
|
2
|
+
/** Full URL of the published post (e.g. "/guides/my-post"). */
|
|
3
|
+
url: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function PostPreview({ url, className }: PostPreviewProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=post-preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-preview.d.ts","sourceRoot":"","sources":["../../src/ui/post-preview.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CAiC/D"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// hazo_blog/src/ui/post-preview.tsx — iframe-based published-article
|
|
4
|
+
// preview for the admin edit dialog's side panel. TOKEN-FREE Tailwind
|
|
5
|
+
// (same isolation rule as the rest of src/ui/*).
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import en from "../locales/en.json" with { type: "json" };
|
|
8
|
+
import { createPackageT } from "hazo_core/i18n";
|
|
9
|
+
const t = createPackageT("hazo_blog", en);
|
|
10
|
+
export function PostPreview({ url, className }) {
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
return (_jsxs("div", { className: `flex h-full flex-col ${className ?? ""}`, children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-100 px-4 py-2 dark:border-zinc-800", children: [_jsx("span", { className: "text-xs font-medium text-zinc-500 dark:text-zinc-400", children: t("post_preview.heading") }), _jsxs("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "text-xs font-medium text-zinc-600 hover:text-zinc-900 hover:underline dark:text-zinc-400 dark:hover:text-zinc-200", children: [t("post_preview.open_in_new_tab"), " \u2197"] })] }), _jsxs("div", { className: "relative flex-1", children: [loading && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-zinc-50 dark:bg-zinc-950", children: _jsx("span", { className: "text-sm text-zinc-400 dark:text-zinc-500", children: t("post_preview.loading") }) })), _jsx("iframe", { src: url, title: t("post_preview.iframe_title"), className: "h-full w-full border-0", onLoad: () => setLoading(false) })] })] }));
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_blog",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "SEO-optimized blogging package: posts, categories, tags, MDX content, and GA4/GSC/Bing-ready SEO.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
"build:test-app": "npm run build && cd test-app && npm run build"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"hazo_core": "^2.
|
|
56
|
-
"hazo_connect": "^4.
|
|
57
|
-
"hazo_api": "^2.
|
|
58
|
-
"hazo_files": "^3.
|
|
59
|
-
"hazo_theme": "^1.
|
|
60
|
-
"hazo_ui": "^6.
|
|
61
|
-
"hazo_images": "^1.
|
|
55
|
+
"hazo_core": "^2.4.0",
|
|
56
|
+
"hazo_connect": "^4.3.0",
|
|
57
|
+
"hazo_api": "^2.8.0",
|
|
58
|
+
"hazo_files": "^3.4.0",
|
|
59
|
+
"hazo_theme": "^1.4.0",
|
|
60
|
+
"hazo_ui": "^6.12.0",
|
|
61
|
+
"hazo_images": "^1.11.0",
|
|
62
62
|
"hazo_jobs": "^0.19.0",
|
|
63
|
-
"hazo_auth": "^10.
|
|
64
|
-
"hazo_i18n": "^0.
|
|
63
|
+
"hazo_auth": "^10.19.0",
|
|
64
|
+
"hazo_i18n": "^0.5.0",
|
|
65
65
|
"react": "^18.0.0 || ^19.0.0",
|
|
66
66
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
67
67
|
"next": "^14.0.0 || ^16.0.0"
|
|
@@ -99,11 +99,11 @@
|
|
|
99
99
|
"react": "^19.0.0",
|
|
100
100
|
"react-dom": "^19.0.0",
|
|
101
101
|
"next": "^16.0.10",
|
|
102
|
-
"hazo_core": "^2.
|
|
103
|
-
"hazo_connect": "^4.
|
|
104
|
-
"hazo_api": "^2.
|
|
105
|
-
"hazo_files": "^3.
|
|
106
|
-
"hazo_ui": "^6.
|
|
102
|
+
"hazo_core": "^2.4.0",
|
|
103
|
+
"hazo_connect": "^4.3.0",
|
|
104
|
+
"hazo_api": "^2.8.0",
|
|
105
|
+
"hazo_files": "^3.4.0",
|
|
106
|
+
"hazo_ui": "^6.12.0",
|
|
107
107
|
"tailwindcss": "^4.2.4",
|
|
108
108
|
"@tailwindcss/postcss": "^4.2.4",
|
|
109
109
|
"postcss": "^8.4.49"
|