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 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
+ ```
@@ -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"})}));
@@ -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
+ }