adversity-global-blog-cms-react 1.0.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/README.md +67 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +301 -0
- package/dist/index.umd.cjs +1 -0
- package/dist/styles.css +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Adversity Global Blog CMS React
|
|
2
|
+
|
|
3
|
+
Reusable React UI package for client websites using Adversity Global Blog CMS.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install adversity-global-blog-cms-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { GlobalBlogCMS } from 'adversity-global-blog-cms-react';
|
|
15
|
+
import 'adversity-global-blog-cms-react/styles.css';
|
|
16
|
+
|
|
17
|
+
export default function BlogPage() {
|
|
18
|
+
return (
|
|
19
|
+
<GlobalBlogCMS
|
|
20
|
+
apiUrl="https://global-blog-cms-api.onrender.com/api"
|
|
21
|
+
apiKey="CLIENT_API_KEY"
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Props
|
|
28
|
+
|
|
29
|
+
| Prop | Type | Required | Description |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `apiUrl` | `string` | Yes | Global Blog CMS API base URL. The package safely adds `/api` if missing. |
|
|
32
|
+
| `apiKey` | `string` | Yes | Website API key generated in the CMS. |
|
|
33
|
+
| `theme` | `'light' \| 'dark'` | No | Built-in color theme. Default is `light`. |
|
|
34
|
+
| `pageSize` | `number` | No | Blogs per page. Default is `9`. |
|
|
35
|
+
| `showSubmitForm` | `boolean` | No | Show or hide the write blog form. Default is `true`. |
|
|
36
|
+
| `title` | `string` | No | Blog page title. |
|
|
37
|
+
| `description` | `string` | No | Blog page intro text. |
|
|
38
|
+
| `emptyMessage` | `string` | No | Message shown when there are no published blogs. |
|
|
39
|
+
| `className` | `string` | No | Extra wrapper class. |
|
|
40
|
+
|
|
41
|
+
## What It Includes
|
|
42
|
+
|
|
43
|
+
- Published blog list
|
|
44
|
+
- Blog detail view
|
|
45
|
+
- Write blog form
|
|
46
|
+
- Optional featured image upload
|
|
47
|
+
- Pagination
|
|
48
|
+
- Search
|
|
49
|
+
- Loading state
|
|
50
|
+
- Error state
|
|
51
|
+
- Empty state
|
|
52
|
+
- API key header handling
|
|
53
|
+
|
|
54
|
+
The package only calls public CMS endpoints. It never uses admin JWTs.
|
|
55
|
+
|
|
56
|
+
## Local Preview
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Set these values for preview:
|
|
63
|
+
|
|
64
|
+
```env
|
|
65
|
+
VITE_CMS_API_BASE_URL=http://127.0.0.1:5000/api
|
|
66
|
+
VITE_CMS_API_KEY=CLIENT_API_KEY
|
|
67
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type GlobalBlogCMSTag = string;
|
|
4
|
+
|
|
5
|
+
export interface GlobalBlogCMSBlog {
|
|
6
|
+
_id: string;
|
|
7
|
+
websiteId?: string;
|
|
8
|
+
title: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
featuredImage?: string | null;
|
|
11
|
+
authorName: string;
|
|
12
|
+
authorEmail: string;
|
|
13
|
+
category: string;
|
|
14
|
+
content: string;
|
|
15
|
+
tags: GlobalBlogCMSTag[];
|
|
16
|
+
status: 'PENDING' | 'PUBLISHED';
|
|
17
|
+
createdAt: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GlobalBlogCMSProps {
|
|
22
|
+
apiUrl: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
theme?: 'light' | 'dark';
|
|
26
|
+
pageSize?: number;
|
|
27
|
+
showSubmitForm?: boolean;
|
|
28
|
+
title?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
emptyMessage?: string;
|
|
31
|
+
renderHeader?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare function GlobalBlogCMS(props: GlobalBlogCMSProps): JSX.Element;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
var J = Object.defineProperty;
|
|
2
|
+
var Q = (a, s, c) => s in a ? J(a, s, { enumerable: !0, configurable: !0, writable: !0, value: c }) : a[s] = c;
|
|
3
|
+
var k = (a, s, c) => Q(a, typeof s != "symbol" ? s + "" : s, c);
|
|
4
|
+
import { jsxs as n, jsx as e, Fragment as x } from "react/jsx-runtime";
|
|
5
|
+
import { useMemo as V, useState as y, useEffect as Y } from "react";
|
|
6
|
+
class I extends Error {
|
|
7
|
+
constructor(c, d, N) {
|
|
8
|
+
super(c);
|
|
9
|
+
k(this, "status");
|
|
10
|
+
k(this, "errors");
|
|
11
|
+
this.name = "BlogApiError", this.status = d, this.errors = N;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const _ = (a) => {
|
|
15
|
+
const s = a.replace(/\/+$/, "");
|
|
16
|
+
return s.endsWith("/api") ? s : `${s}/api`;
|
|
17
|
+
}, X = (a) => {
|
|
18
|
+
var d, N, t;
|
|
19
|
+
const s = new URLSearchParams();
|
|
20
|
+
a.page && s.set("page", String(a.page)), a.limit && s.set("limit", String(a.limit)), (d = a.search) != null && d.trim() && s.set("search", a.search.trim()), (N = a.category) != null && N.trim() && s.set("category", a.category.trim()), (t = a.tag) != null && t.trim() && s.set("tag", a.tag.trim());
|
|
21
|
+
const c = s.toString();
|
|
22
|
+
return c ? `?${c}` : "";
|
|
23
|
+
}, Z = (a, s) => {
|
|
24
|
+
const c = _(a), d = async (t, g = {}) => {
|
|
25
|
+
var u;
|
|
26
|
+
if (!s)
|
|
27
|
+
throw new I("Missing Global Blog CMS API key.", 401);
|
|
28
|
+
const i = new Headers(g.headers);
|
|
29
|
+
i.set("x-api-key", s), !(g.body instanceof FormData) && !i.has("Content-Type") && i.set("Content-Type", "application/json");
|
|
30
|
+
let f;
|
|
31
|
+
try {
|
|
32
|
+
f = await fetch(`${c}${t}`, {
|
|
33
|
+
...g,
|
|
34
|
+
headers: i
|
|
35
|
+
});
|
|
36
|
+
} catch (r) {
|
|
37
|
+
throw new I(r instanceof Error ? r.message : "CMS API request failed.", 0);
|
|
38
|
+
}
|
|
39
|
+
const o = await f.json().catch(() => null);
|
|
40
|
+
if (!f.ok || !(o != null && o.success)) {
|
|
41
|
+
const r = (u = o == null ? void 0 : o.errors) == null ? void 0 : u.map((m) => m.message).join(", ");
|
|
42
|
+
throw new I(
|
|
43
|
+
r || (o == null ? void 0 : o.message) || "CMS API request failed.",
|
|
44
|
+
f.status,
|
|
45
|
+
o == null ? void 0 : o.errors
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return o;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
imageUrl: (t) => t ? /^https?:\/\//i.test(t) ? t : `${c.replace(/\/api$/, "")}/${t.replace(/^\/+/, "")}` : "",
|
|
52
|
+
async getBlogs(t = {}) {
|
|
53
|
+
var i;
|
|
54
|
+
const g = await d(`/blogs${X(t)}`);
|
|
55
|
+
return {
|
|
56
|
+
blogs: ((i = g.data) == null ? void 0 : i.blogs) || [],
|
|
57
|
+
meta: g.meta
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
async getLatestBlogs(t = 5) {
|
|
61
|
+
var i;
|
|
62
|
+
return ((i = (await d(`/blogs/latest?limit=${t}`)).data) == null ? void 0 : i.blogs) || [];
|
|
63
|
+
},
|
|
64
|
+
async getBlog(t) {
|
|
65
|
+
var i;
|
|
66
|
+
const g = await d(`/blogs/${encodeURIComponent(t)}`);
|
|
67
|
+
if (!((i = g.data) != null && i.blog))
|
|
68
|
+
throw new I("Blog not found.", 404);
|
|
69
|
+
return g.data.blog;
|
|
70
|
+
},
|
|
71
|
+
async submitBlog(t) {
|
|
72
|
+
var f, o, u;
|
|
73
|
+
if (!!t.featuredImage) {
|
|
74
|
+
const r = new FormData();
|
|
75
|
+
r.set("title", t.title), r.set("authorName", t.authorName), r.set("authorEmail", t.authorEmail), r.set("category", t.category), r.set("content", t.content), (f = t.tags) != null && f.length && r.set("tags", t.tags.join(",")), t.featuredImage && r.set("featuredImage", t.featuredImage);
|
|
76
|
+
const m = await d("/blogs/submit", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: r
|
|
79
|
+
});
|
|
80
|
+
if (!((o = m.data) != null && o.blog)) throw new I("Blog submission response is missing data.", 500);
|
|
81
|
+
return m.data.blog;
|
|
82
|
+
}
|
|
83
|
+
const i = await d("/blogs/submit", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
title: t.title,
|
|
87
|
+
authorName: t.authorName,
|
|
88
|
+
authorEmail: t.authorEmail,
|
|
89
|
+
category: t.category,
|
|
90
|
+
content: t.content,
|
|
91
|
+
tags: t.tags || []
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
if (!((u = i.data) != null && u.blog)) throw new I("Blog submission response is missing data.", 500);
|
|
95
|
+
return i.data.blog;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}, O = (a) => new Intl.DateTimeFormat(void 0, {
|
|
99
|
+
year: "numeric",
|
|
100
|
+
month: "short",
|
|
101
|
+
day: "numeric"
|
|
102
|
+
}).format(new Date(a)), K = (a) => a.replace(/\s+/g, " ").trim().slice(0, 180), ee = (a) => a.split(",").map((s) => s.trim()).filter(Boolean);
|
|
103
|
+
function re({
|
|
104
|
+
apiUrl: a,
|
|
105
|
+
apiKey: s,
|
|
106
|
+
className: c = "",
|
|
107
|
+
theme: d = "light",
|
|
108
|
+
pageSize: N = 9,
|
|
109
|
+
showSubmitForm: t = !0,
|
|
110
|
+
title: g = "Blogs",
|
|
111
|
+
description: i = "Read the latest published articles or submit your own story for review.",
|
|
112
|
+
emptyMessage: f = "No published blogs are available yet.",
|
|
113
|
+
renderHeader: o
|
|
114
|
+
}) {
|
|
115
|
+
const u = V(() => Z(a, s), [a, s]), [r, m] = y({ name: "list", page: 1 }), [A, $] = y([]), [b, P] = y(null), [C, D] = y(1), [L, M] = y(""), [E, T] = y(""), [S, v] = y(!1), [U, B] = y(""), [q, j] = y(""), R = async (l, w) => {
|
|
116
|
+
var h;
|
|
117
|
+
v(!0), B("");
|
|
118
|
+
try {
|
|
119
|
+
const p = await u.getBlogs({
|
|
120
|
+
page: l,
|
|
121
|
+
limit: N,
|
|
122
|
+
search: w
|
|
123
|
+
});
|
|
124
|
+
$(p.blogs), D(((h = p.meta) == null ? void 0 : h.totalPages) || 1);
|
|
125
|
+
} catch (p) {
|
|
126
|
+
B(p instanceof Error ? p.message : "Unable to load blogs."), $([]), D(1);
|
|
127
|
+
} finally {
|
|
128
|
+
v(!1);
|
|
129
|
+
}
|
|
130
|
+
}, W = async (l) => {
|
|
131
|
+
v(!0), B(""), P(null);
|
|
132
|
+
try {
|
|
133
|
+
P(await u.getBlog(l));
|
|
134
|
+
} catch (w) {
|
|
135
|
+
B(w instanceof Error ? w.message : "Unable to load blog.");
|
|
136
|
+
} finally {
|
|
137
|
+
v(!1);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
Y(() => {
|
|
141
|
+
r.name === "list" && R(r.page, E), r.name === "detail" && W(r.slug);
|
|
142
|
+
}, [r, E, N, u]);
|
|
143
|
+
const z = (l) => {
|
|
144
|
+
l.preventDefault(), T(L), m({ name: "list", page: 1 });
|
|
145
|
+
}, G = async (l) => {
|
|
146
|
+
l.preventDefault(), v(!0), B(""), j("");
|
|
147
|
+
const w = l.currentTarget, h = new FormData(w), p = h.get("featuredImage"), H = {
|
|
148
|
+
title: String(h.get("title") || ""),
|
|
149
|
+
authorName: String(h.get("authorName") || ""),
|
|
150
|
+
authorEmail: String(h.get("authorEmail") || ""),
|
|
151
|
+
category: String(h.get("category") || ""),
|
|
152
|
+
content: String(h.get("content") || ""),
|
|
153
|
+
tags: ee(String(h.get("tags") || "")),
|
|
154
|
+
featuredImage: p instanceof File && p.size > 0 ? p : null
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
await u.submitBlog(H), w.reset(), j("Your blog has been submitted for approval.");
|
|
158
|
+
} catch (F) {
|
|
159
|
+
B(F instanceof Error ? F.message : "Unable to submit blog.");
|
|
160
|
+
} finally {
|
|
161
|
+
v(!1);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
return /* @__PURE__ */ n("section", { className: `gbcms-widget gbcms-theme-${d} ${c}`.trim(), children: [
|
|
165
|
+
/* @__PURE__ */ e("header", { className: "gbcms-header", children: o || /* @__PURE__ */ n(x, { children: [
|
|
166
|
+
/* @__PURE__ */ n("div", { children: [
|
|
167
|
+
/* @__PURE__ */ e("h1", { children: g }),
|
|
168
|
+
/* @__PURE__ */ e("p", { children: i })
|
|
169
|
+
] }),
|
|
170
|
+
t && /* @__PURE__ */ e("button", { className: "gbcms-button gbcms-button-primary", type: "button", onClick: () => m({ name: "write" }), children: "Write Blog" })
|
|
171
|
+
] }) }),
|
|
172
|
+
U && /* @__PURE__ */ e("div", { className: "gbcms-alert gbcms-alert-error", children: U }),
|
|
173
|
+
q && /* @__PURE__ */ e("div", { className: "gbcms-alert gbcms-alert-success", children: q }),
|
|
174
|
+
r.name === "list" && /* @__PURE__ */ n(x, { children: [
|
|
175
|
+
/* @__PURE__ */ n("form", { className: "gbcms-toolbar", onSubmit: z, children: [
|
|
176
|
+
/* @__PURE__ */ e(
|
|
177
|
+
"input",
|
|
178
|
+
{
|
|
179
|
+
value: L,
|
|
180
|
+
onChange: (l) => M(l.target.value),
|
|
181
|
+
placeholder: "Search blogs",
|
|
182
|
+
"aria-label": "Search blogs"
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
/* @__PURE__ */ e("button", { className: "gbcms-button", type: "submit", children: "Search" }),
|
|
186
|
+
E && /* @__PURE__ */ e(
|
|
187
|
+
"button",
|
|
188
|
+
{
|
|
189
|
+
className: "gbcms-button gbcms-button-muted",
|
|
190
|
+
type: "button",
|
|
191
|
+
onClick: () => {
|
|
192
|
+
M(""), T(""), m({ name: "list", page: 1 });
|
|
193
|
+
},
|
|
194
|
+
children: "Clear"
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
] }),
|
|
198
|
+
S ? /* @__PURE__ */ e("div", { className: "gbcms-state", children: "Loading blogs..." }) : A.length === 0 ? /* @__PURE__ */ e("div", { className: "gbcms-state", children: f }) : /* @__PURE__ */ e("div", { className: "gbcms-grid", children: A.map((l) => /* @__PURE__ */ n("article", { className: "gbcms-card", children: [
|
|
199
|
+
l.featuredImage && /* @__PURE__ */ e("img", { className: "gbcms-card-image", src: u.imageUrl(l.featuredImage), alt: l.title, loading: "lazy" }),
|
|
200
|
+
/* @__PURE__ */ n("div", { className: "gbcms-card-body", children: [
|
|
201
|
+
/* @__PURE__ */ n("div", { className: "gbcms-meta", children: [
|
|
202
|
+
/* @__PURE__ */ e("span", { children: l.category }),
|
|
203
|
+
/* @__PURE__ */ e("span", { children: O(l.createdAt) })
|
|
204
|
+
] }),
|
|
205
|
+
/* @__PURE__ */ e("h2", { children: l.title }),
|
|
206
|
+
/* @__PURE__ */ e("p", { children: K(l.content) }),
|
|
207
|
+
/* @__PURE__ */ e("button", { className: "gbcms-link-button", type: "button", onClick: () => m({ name: "detail", slug: l.slug }), children: "Read more" })
|
|
208
|
+
] })
|
|
209
|
+
] }, l._id)) }),
|
|
210
|
+
C > 1 && /* @__PURE__ */ n("nav", { className: "gbcms-pagination", "aria-label": "Blog pagination", children: [
|
|
211
|
+
/* @__PURE__ */ e(
|
|
212
|
+
"button",
|
|
213
|
+
{
|
|
214
|
+
className: "gbcms-button",
|
|
215
|
+
type: "button",
|
|
216
|
+
disabled: r.page <= 1 || S,
|
|
217
|
+
onClick: () => m({ name: "list", page: Math.max(1, r.page - 1) }),
|
|
218
|
+
children: "Previous"
|
|
219
|
+
}
|
|
220
|
+
),
|
|
221
|
+
/* @__PURE__ */ n("span", { children: [
|
|
222
|
+
"Page ",
|
|
223
|
+
r.page,
|
|
224
|
+
" of ",
|
|
225
|
+
C
|
|
226
|
+
] }),
|
|
227
|
+
/* @__PURE__ */ e(
|
|
228
|
+
"button",
|
|
229
|
+
{
|
|
230
|
+
className: "gbcms-button",
|
|
231
|
+
type: "button",
|
|
232
|
+
disabled: r.page >= C || S,
|
|
233
|
+
onClick: () => m({ name: "list", page: Math.min(C, r.page + 1) }),
|
|
234
|
+
children: "Next"
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
] })
|
|
238
|
+
] }),
|
|
239
|
+
r.name === "detail" && /* @__PURE__ */ n("article", { className: "gbcms-detail", children: [
|
|
240
|
+
/* @__PURE__ */ e("button", { className: "gbcms-link-button", type: "button", onClick: () => m({ name: "list", page: 1 }), children: "Back to blogs" }),
|
|
241
|
+
S ? /* @__PURE__ */ e("div", { className: "gbcms-state", children: "Loading blog..." }) : b ? /* @__PURE__ */ n(x, { children: [
|
|
242
|
+
b.featuredImage && /* @__PURE__ */ e("img", { className: "gbcms-detail-image", src: u.imageUrl(b.featuredImage), alt: b.title }),
|
|
243
|
+
/* @__PURE__ */ n("div", { className: "gbcms-meta", children: [
|
|
244
|
+
/* @__PURE__ */ e("span", { children: b.category }),
|
|
245
|
+
/* @__PURE__ */ e("span", { children: O(b.createdAt) }),
|
|
246
|
+
/* @__PURE__ */ n("span", { children: [
|
|
247
|
+
"By ",
|
|
248
|
+
b.authorName
|
|
249
|
+
] })
|
|
250
|
+
] }),
|
|
251
|
+
/* @__PURE__ */ e("h1", { children: b.title }),
|
|
252
|
+
/* @__PURE__ */ e("div", { className: "gbcms-tags", children: b.tags.map((l) => /* @__PURE__ */ e("span", { children: l }, l)) }),
|
|
253
|
+
/* @__PURE__ */ e("div", { className: "gbcms-content", children: b.content })
|
|
254
|
+
] }) : /* @__PURE__ */ e("div", { className: "gbcms-state", children: "Blog not found." })
|
|
255
|
+
] }),
|
|
256
|
+
r.name === "write" && t && /* @__PURE__ */ n("form", { className: "gbcms-form", onSubmit: G, children: [
|
|
257
|
+
/* @__PURE__ */ n("div", { className: "gbcms-form-heading", children: [
|
|
258
|
+
/* @__PURE__ */ e("button", { className: "gbcms-link-button", type: "button", onClick: () => m({ name: "list", page: 1 }), children: "Back to blogs" }),
|
|
259
|
+
/* @__PURE__ */ e("h2", { children: "Write a Blog" }),
|
|
260
|
+
/* @__PURE__ */ e("p", { children: "Submitted blogs are sent to the website admin for approval before publishing." })
|
|
261
|
+
] }),
|
|
262
|
+
/* @__PURE__ */ n("label", { children: [
|
|
263
|
+
"Title",
|
|
264
|
+
/* @__PURE__ */ e("input", { name: "title", required: !0, maxLength: 160 })
|
|
265
|
+
] }),
|
|
266
|
+
/* @__PURE__ */ n("div", { className: "gbcms-form-row", children: [
|
|
267
|
+
/* @__PURE__ */ n("label", { children: [
|
|
268
|
+
"Author name",
|
|
269
|
+
/* @__PURE__ */ e("input", { name: "authorName", required: !0, maxLength: 100 })
|
|
270
|
+
] }),
|
|
271
|
+
/* @__PURE__ */ n("label", { children: [
|
|
272
|
+
"Author email",
|
|
273
|
+
/* @__PURE__ */ e("input", { name: "authorEmail", required: !0, type: "email", maxLength: 160 })
|
|
274
|
+
] })
|
|
275
|
+
] }),
|
|
276
|
+
/* @__PURE__ */ n("label", { children: [
|
|
277
|
+
"Category",
|
|
278
|
+
/* @__PURE__ */ e("input", { name: "category", required: !0, maxLength: 80 })
|
|
279
|
+
] }),
|
|
280
|
+
/* @__PURE__ */ n("label", { children: [
|
|
281
|
+
"Tags",
|
|
282
|
+
/* @__PURE__ */ e("input", { name: "tags", placeholder: "SEO, Web Design, Marketing" })
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ n("label", { children: [
|
|
285
|
+
"Featured image",
|
|
286
|
+
/* @__PURE__ */ e("input", { name: "featuredImage", type: "file", accept: "image/jpeg,image/jpg,image/png,image/webp" })
|
|
287
|
+
] }),
|
|
288
|
+
/* @__PURE__ */ n("label", { children: [
|
|
289
|
+
"Content",
|
|
290
|
+
/* @__PURE__ */ e("textarea", { name: "content", required: !0, rows: 10, minLength: 50 })
|
|
291
|
+
] }),
|
|
292
|
+
/* @__PURE__ */ e("button", { className: "gbcms-button gbcms-button-primary", type: "submit", disabled: S, children: S ? "Submitting..." : "Submit for Approval" })
|
|
293
|
+
] })
|
|
294
|
+
] });
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
I as BlogApiError,
|
|
298
|
+
re as GlobalBlogCMS,
|
|
299
|
+
Z as createBlogApi,
|
|
300
|
+
_ as normalizeApiUrl
|
|
301
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(n,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],e):(n=typeof globalThis<"u"?globalThis:n||self,e(n.GlobalBlogCMSReact={},n.jsxRuntime,n.React))})(this,(function(n,e,i){"use strict";var Z=Object.defineProperty;var K=(n,e,i)=>e in n?Z(n,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):n[e]=i;var k=(n,e,i)=>K(n,typeof e!="symbol"?e+"":e,i);class N extends Error{constructor(f,m,S){super(f);k(this,"status");k(this,"errors");this.name="BlogApiError",this.status=m,this.errors=S}}const A=s=>{const l=s.replace(/\/+$/,"");return l.endsWith("/api")?l:`${l}/api`},W=s=>{var m,S,t;const l=new URLSearchParams;s.page&&l.set("page",String(s.page)),s.limit&&l.set("limit",String(s.limit)),(m=s.search)!=null&&m.trim()&&l.set("search",s.search.trim()),(S=s.category)!=null&&S.trim()&&l.set("category",s.category.trim()),(t=s.tag)!=null&&t.trim()&&l.set("tag",s.tag.trim());const f=l.toString();return f?`?${f}`:""},M=(s,l)=>{const f=A(s),m=async(t,g={})=>{var b;if(!l)throw new N("Missing Global Blog CMS API key.",401);const o=new Headers(g.headers);o.set("x-api-key",l),!(g.body instanceof FormData)&&!o.has("Content-Type")&&o.set("Content-Type","application/json");let p;try{p=await fetch(`${f}${t}`,{...g,headers:o})}catch(a){throw new N(a instanceof Error?a.message:"CMS API request failed.",0)}const c=await p.json().catch(()=>null);if(!p.ok||!(c!=null&&c.success)){const a=(b=c==null?void 0:c.errors)==null?void 0:b.map(d=>d.message).join(", ");throw new N(a||(c==null?void 0:c.message)||"CMS API request failed.",p.status,c==null?void 0:c.errors)}return c};return{imageUrl:t=>t?/^https?:\/\//i.test(t)?t:`${f.replace(/\/api$/,"")}/${t.replace(/^\/+/,"")}`:"",async getBlogs(t={}){var o;const g=await m(`/blogs${W(t)}`);return{blogs:((o=g.data)==null?void 0:o.blogs)||[],meta:g.meta}},async getLatestBlogs(t=5){var o;return((o=(await m(`/blogs/latest?limit=${t}`)).data)==null?void 0:o.blogs)||[]},async getBlog(t){var o;const g=await m(`/blogs/${encodeURIComponent(t)}`);if(!((o=g.data)!=null&&o.blog))throw new N("Blog not found.",404);return g.data.blog},async submitBlog(t){var p,c,b;if(!!t.featuredImage){const a=new FormData;a.set("title",t.title),a.set("authorName",t.authorName),a.set("authorEmail",t.authorEmail),a.set("category",t.category),a.set("content",t.content),(p=t.tags)!=null&&p.length&&a.set("tags",t.tags.join(",")),t.featuredImage&&a.set("featuredImage",t.featuredImage);const d=await m("/blogs/submit",{method:"POST",body:a});if(!((c=d.data)!=null&&c.blog))throw new N("Blog submission response is missing data.",500);return d.data.blog}const o=await m("/blogs/submit",{method:"POST",body:JSON.stringify({title:t.title,authorName:t.authorName,authorEmail:t.authorEmail,category:t.category,content:t.content,tags:t.tags||[]})});if(!((b=o.data)!=null&&b.blog))throw new N("Blog submission response is missing data.",500);return o.data.blog}}},T=s=>new Intl.DateTimeFormat(void 0,{year:"numeric",month:"short",day:"numeric"}).format(new Date(s)),z=s=>s.replace(/\s+/g," ").trim().slice(0,180),H=s=>s.split(",").map(l=>l.trim()).filter(Boolean);function J({apiUrl:s,apiKey:l,className:f="",theme:m="light",pageSize:S=9,showSubmitForm:t=!0,title:g="Blogs",description:o="Read the latest published articles or submit your own story for review.",emptyMessage:p="No published blogs are available yet.",renderHeader:c}){const b=i.useMemo(()=>M(s,l),[s,l]),[a,d]=i.useState({name:"list",page:1}),[$,P]=i.useState([]),[h,q]=i.useState(null),[I,U]=i.useState(1),[x,D]=i.useState(""),[E,L]=i.useState(""),[B,v]=i.useState(!1),[j,C]=i.useState(""),[F,O]=i.useState(""),Q=async(r,w)=>{var u;v(!0),C("");try{const y=await b.getBlogs({page:r,limit:S,search:w});P(y.blogs),U(((u=y.meta)==null?void 0:u.totalPages)||1)}catch(y){C(y instanceof Error?y.message:"Unable to load blogs."),P([]),U(1)}finally{v(!1)}},V=async r=>{v(!0),C(""),q(null);try{q(await b.getBlog(r))}catch(w){C(w instanceof Error?w.message:"Unable to load blog.")}finally{v(!1)}};i.useEffect(()=>{a.name==="list"&&Q(a.page,E),a.name==="detail"&&V(a.slug)},[a,E,S,b]);const Y=r=>{r.preventDefault(),L(x),d({name:"list",page:1})},_=async r=>{r.preventDefault(),v(!0),C(""),O("");const w=r.currentTarget,u=new FormData(w),y=u.get("featuredImage"),X={title:String(u.get("title")||""),authorName:String(u.get("authorName")||""),authorEmail:String(u.get("authorEmail")||""),category:String(u.get("category")||""),content:String(u.get("content")||""),tags:H(String(u.get("tags")||"")),featuredImage:y instanceof File&&y.size>0?y:null};try{await b.submitBlog(X),w.reset(),O("Your blog has been submitted for approval.")}catch(G){C(G instanceof Error?G.message:"Unable to submit blog.")}finally{v(!1)}};return e.jsxs("section",{className:`gbcms-widget gbcms-theme-${m} ${f}`.trim(),children:[e.jsx("header",{className:"gbcms-header",children:c||e.jsxs(e.Fragment,{children:[e.jsxs("div",{children:[e.jsx("h1",{children:g}),e.jsx("p",{children:o})]}),t&&e.jsx("button",{className:"gbcms-button gbcms-button-primary",type:"button",onClick:()=>d({name:"write"}),children:"Write Blog"})]})}),j&&e.jsx("div",{className:"gbcms-alert gbcms-alert-error",children:j}),F&&e.jsx("div",{className:"gbcms-alert gbcms-alert-success",children:F}),a.name==="list"&&e.jsxs(e.Fragment,{children:[e.jsxs("form",{className:"gbcms-toolbar",onSubmit:Y,children:[e.jsx("input",{value:x,onChange:r=>D(r.target.value),placeholder:"Search blogs","aria-label":"Search blogs"}),e.jsx("button",{className:"gbcms-button",type:"submit",children:"Search"}),E&&e.jsx("button",{className:"gbcms-button gbcms-button-muted",type:"button",onClick:()=>{D(""),L(""),d({name:"list",page:1})},children:"Clear"})]}),B?e.jsx("div",{className:"gbcms-state",children:"Loading blogs..."}):$.length===0?e.jsx("div",{className:"gbcms-state",children:p}):e.jsx("div",{className:"gbcms-grid",children:$.map(r=>e.jsxs("article",{className:"gbcms-card",children:[r.featuredImage&&e.jsx("img",{className:"gbcms-card-image",src:b.imageUrl(r.featuredImage),alt:r.title,loading:"lazy"}),e.jsxs("div",{className:"gbcms-card-body",children:[e.jsxs("div",{className:"gbcms-meta",children:[e.jsx("span",{children:r.category}),e.jsx("span",{children:T(r.createdAt)})]}),e.jsx("h2",{children:r.title}),e.jsx("p",{children:z(r.content)}),e.jsx("button",{className:"gbcms-link-button",type:"button",onClick:()=>d({name:"detail",slug:r.slug}),children:"Read more"})]})]},r._id))}),I>1&&e.jsxs("nav",{className:"gbcms-pagination","aria-label":"Blog pagination",children:[e.jsx("button",{className:"gbcms-button",type:"button",disabled:a.page<=1||B,onClick:()=>d({name:"list",page:Math.max(1,a.page-1)}),children:"Previous"}),e.jsxs("span",{children:["Page ",a.page," of ",I]}),e.jsx("button",{className:"gbcms-button",type:"button",disabled:a.page>=I||B,onClick:()=>d({name:"list",page:Math.min(I,a.page+1)}),children:"Next"})]})]}),a.name==="detail"&&e.jsxs("article",{className:"gbcms-detail",children:[e.jsx("button",{className:"gbcms-link-button",type:"button",onClick:()=>d({name:"list",page:1}),children:"Back to blogs"}),B?e.jsx("div",{className:"gbcms-state",children:"Loading blog..."}):h?e.jsxs(e.Fragment,{children:[h.featuredImage&&e.jsx("img",{className:"gbcms-detail-image",src:b.imageUrl(h.featuredImage),alt:h.title}),e.jsxs("div",{className:"gbcms-meta",children:[e.jsx("span",{children:h.category}),e.jsx("span",{children:T(h.createdAt)}),e.jsxs("span",{children:["By ",h.authorName]})]}),e.jsx("h1",{children:h.title}),e.jsx("div",{className:"gbcms-tags",children:h.tags.map(r=>e.jsx("span",{children:r},r))}),e.jsx("div",{className:"gbcms-content",children:h.content})]}):e.jsx("div",{className:"gbcms-state",children:"Blog not found."})]}),a.name==="write"&&t&&e.jsxs("form",{className:"gbcms-form",onSubmit:_,children:[e.jsxs("div",{className:"gbcms-form-heading",children:[e.jsx("button",{className:"gbcms-link-button",type:"button",onClick:()=>d({name:"list",page:1}),children:"Back to blogs"}),e.jsx("h2",{children:"Write a Blog"}),e.jsx("p",{children:"Submitted blogs are sent to the website admin for approval before publishing."})]}),e.jsxs("label",{children:["Title",e.jsx("input",{name:"title",required:!0,maxLength:160})]}),e.jsxs("div",{className:"gbcms-form-row",children:[e.jsxs("label",{children:["Author name",e.jsx("input",{name:"authorName",required:!0,maxLength:100})]}),e.jsxs("label",{children:["Author email",e.jsx("input",{name:"authorEmail",required:!0,type:"email",maxLength:160})]})]}),e.jsxs("label",{children:["Category",e.jsx("input",{name:"category",required:!0,maxLength:80})]}),e.jsxs("label",{children:["Tags",e.jsx("input",{name:"tags",placeholder:"SEO, Web Design, Marketing"})]}),e.jsxs("label",{children:["Featured image",e.jsx("input",{name:"featuredImage",type:"file",accept:"image/jpeg,image/jpg,image/png,image/webp"})]}),e.jsxs("label",{children:["Content",e.jsx("textarea",{name:"content",required:!0,rows:10,minLength:50})]}),e.jsx("button",{className:"gbcms-button gbcms-button-primary",type:"submit",disabled:B,children:B?"Submitting...":"Submit for Approval"})]})]})}n.BlogApiError=N,n.GlobalBlogCMS=J,n.createBlogApi=M,n.normalizeApiUrl=A,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}));
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.gbcms-widget{--gbcms-bg: #ffffff;--gbcms-surface: #ffffff;--gbcms-text: #111827;--gbcms-muted: #667085;--gbcms-border: #d9e1ec;--gbcms-primary: #2563eb;--gbcms-primary-hover: #1d4ed8;--gbcms-success-bg: #ecfdf3;--gbcms-success-text: #027a48;--gbcms-error-bg: #fff1f3;--gbcms-error-text: #c01048;color:var(--gbcms-text);background:var(--gbcms-bg);font-family:inherit;line-height:1.5;width:100%}.gbcms-theme-dark{--gbcms-bg: #0f172a;--gbcms-surface: #111c33;--gbcms-text: #f8fafc;--gbcms-muted: #b6c2d2;--gbcms-border: #2c3a52;--gbcms-primary: #60a5fa;--gbcms-primary-hover: #93c5fd}.gbcms-widget *,.gbcms-widget *:before,.gbcms-widget *:after{box-sizing:border-box}.gbcms-header{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:28px}.gbcms-header h1,.gbcms-detail h1,.gbcms-form h2,.gbcms-card h2{color:var(--gbcms-text);letter-spacing:0;margin:0}.gbcms-header h1{font-size:clamp(32px,5vw,54px);line-height:1.08}.gbcms-header p,.gbcms-form-heading p,.gbcms-card p{color:var(--gbcms-muted);margin:8px 0 0}.gbcms-button,.gbcms-link-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--gbcms-border);border-radius:8px;cursor:pointer;font:inherit;min-height:42px;padding:9px 16px;transition:background-color .16s ease,border-color .16s ease,color .16s ease,opacity .16s ease}.gbcms-button{background:var(--gbcms-surface);color:var(--gbcms-text)}.gbcms-button:hover:not(:disabled){border-color:var(--gbcms-primary);color:var(--gbcms-primary)}.gbcms-button:disabled{cursor:not-allowed;opacity:.55}.gbcms-button-primary{background:var(--gbcms-primary);border-color:var(--gbcms-primary);color:#fff}.gbcms-button-primary:hover:not(:disabled){background:var(--gbcms-primary-hover);border-color:var(--gbcms-primary-hover);color:#fff}.gbcms-button-muted{color:var(--gbcms-muted)}.gbcms-link-button{background:transparent;border-color:transparent;color:var(--gbcms-primary);min-height:auto;padding:0}.gbcms-link-button:hover{color:var(--gbcms-primary-hover);text-decoration:underline}.gbcms-alert{border-radius:8px;margin-bottom:18px;padding:12px 14px}.gbcms-alert-error{background:var(--gbcms-error-bg);color:var(--gbcms-error-text)}.gbcms-alert-success{background:var(--gbcms-success-bg);color:var(--gbcms-success-text)}.gbcms-toolbar{display:flex;gap:10px;margin-bottom:24px}.gbcms-toolbar input,.gbcms-form input,.gbcms-form textarea{background:var(--gbcms-surface);border:1px solid var(--gbcms-border);border-radius:8px;color:var(--gbcms-text);font:inherit;min-height:44px;padding:10px 12px;width:100%}.gbcms-toolbar input:focus,.gbcms-form input:focus,.gbcms-form textarea:focus{border-color:var(--gbcms-primary);outline:2px solid color-mix(in srgb,var(--gbcms-primary) 20%,transparent)}.gbcms-grid{display:grid;gap:20px;grid-template-columns:repeat(3,minmax(0,1fr))}.gbcms-card{background:var(--gbcms-surface);border:1px solid var(--gbcms-border);border-radius:8px;overflow:hidden}.gbcms-card-image{aspect-ratio:16 / 9;display:block;object-fit:cover;width:100%}.gbcms-card-body{padding:18px}.gbcms-card h2{font-size:20px;line-height:1.25;margin-top:10px}.gbcms-card p{margin-bottom:16px}.gbcms-meta{color:var(--gbcms-muted);display:flex;flex-wrap:wrap;font-size:14px;gap:10px}.gbcms-meta span:not(:last-child):after{color:var(--gbcms-border);content:"/";margin-left:10px}.gbcms-state{border:1px dashed var(--gbcms-border);border-radius:8px;color:var(--gbcms-muted);padding:34px;text-align:center}.gbcms-pagination{align-items:center;color:var(--gbcms-muted);display:flex;gap:14px;justify-content:center;margin-top:28px}.gbcms-detail{max-width:860px}.gbcms-detail-image{aspect-ratio:16 / 9;border-radius:8px;display:block;margin:18px 0 22px;object-fit:cover;width:100%}.gbcms-detail h1{font-size:clamp(30px,4vw,48px);line-height:1.12;margin:12px 0 14px}.gbcms-tags{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:22px}.gbcms-tags span{background:color-mix(in srgb,var(--gbcms-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--gbcms-primary) 18%,transparent);border-radius:999px;color:var(--gbcms-primary);font-size:13px;padding:4px 10px}.gbcms-content{color:var(--gbcms-text);font-size:18px;white-space:pre-wrap}.gbcms-form{background:var(--gbcms-surface);border:1px solid var(--gbcms-border);border-radius:8px;display:grid;gap:16px;max-width:820px;padding:24px}.gbcms-form-heading{margin-bottom:4px}.gbcms-form-heading h2{font-size:28px;margin-top:8px}.gbcms-form label{color:var(--gbcms-text);display:grid;font-weight:600;gap:7px}.gbcms-form-row{display:grid;gap:16px;grid-template-columns:repeat(2,minmax(0,1fr))}.gbcms-form textarea{min-height:220px;resize:vertical}@media(max-width:860px){.gbcms-header,.gbcms-toolbar{flex-direction:column}.gbcms-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.gbcms-grid,.gbcms-form-row{grid-template-columns:1fr}.gbcms-header h1{font-size:34px}.gbcms-form{padding:18px}}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adversity-global-blog-cms-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reusable Adversity Global Blog CMS React UI for client websites.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.umd.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.umd.cjs"
|
|
18
|
+
},
|
|
19
|
+
"./styles.css": "./dist/styles.css"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "vite build && node scripts/write-types.mjs",
|
|
23
|
+
"dev": "vite --host 127.0.0.1 --port 5190"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"blog",
|
|
27
|
+
"cms",
|
|
28
|
+
"react",
|
|
29
|
+
"multi-tenant"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"react-dom": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
37
|
+
"vite": "^6.3.5"
|
|
38
|
+
}
|
|
39
|
+
}
|