@dispatchcms/react 0.0.6 → 0.0.8
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 +34 -24
- package/dist/index.d.mts +21 -13
- package/dist/index.d.ts +21 -13
- package/dist/index.js +60 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +57 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/hooks.ts +37 -30
- package/src/index.ts +29 -3
- package/src/types.ts +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dispatchcms/react
|
|
2
2
|
|
|
3
|
-
React hooks and provider for [Dispatch](https://dispatch-cms.vercel.app) CMS. Use `DispatchProvider` and `
|
|
3
|
+
React hooks and provider for [Dispatch](https://dispatch-cms.vercel.app) CMS. Use `DispatchProvider` and `useEntry` / `useEntries` to fetch published entries in any React app (Next.js, Vite, etc.).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -32,29 +32,29 @@ To use a different API base URL, pass the `apiBase` prop to the provider.
|
|
|
32
32
|
|
|
33
33
|
## API
|
|
34
34
|
|
|
35
|
-
### `
|
|
35
|
+
### `useEntry(collection, slug)`
|
|
36
36
|
|
|
37
|
-
Fetches a single published
|
|
37
|
+
Fetches a single published entry.
|
|
38
38
|
|
|
39
|
-
- **Returns:** `{
|
|
40
|
-
- On failure or missing site key, does not throw; returns `
|
|
39
|
+
- **Returns:** `{ entry: Entry | null, isLoading: boolean, error?: string }`
|
|
40
|
+
- On failure or missing site key, does not throw; returns `entry: null`, `isLoading: false`, and `error` set.
|
|
41
41
|
|
|
42
|
-
### `
|
|
42
|
+
### `useEntries(collection)`
|
|
43
43
|
|
|
44
|
-
Fetches
|
|
44
|
+
Fetches published entries for a collection.
|
|
45
45
|
|
|
46
|
-
- **Returns:** `{
|
|
47
|
-
- On failure or missing site key, does not throw; returns `
|
|
46
|
+
- **Returns:** `{ entries: Entry[], isLoading: boolean, error?: string }`
|
|
47
|
+
- On failure or missing site key, does not throw; returns `entries: []`, `isLoading: false`, and `error` set.
|
|
48
48
|
|
|
49
49
|
### Graceful failure
|
|
50
50
|
|
|
51
|
-
- If the API request fails, hooks return `
|
|
51
|
+
- If the API request fails, hooks return `entry: null` / `entries: []`, `isLoading: false`, and `error: "Failed to load"`. The React tree is never crashed.
|
|
52
52
|
- If no `siteKey` is provided to the provider, `console.warn` is logged and hooks return the same safe empty/error state with `error: "Missing site key"`.
|
|
53
53
|
|
|
54
54
|
## Types
|
|
55
55
|
|
|
56
|
-
- **`
|
|
57
|
-
- **`
|
|
56
|
+
- **`Entry`** – Entry from a collection. Fields include `id`, `collection`, `title`, `slug`, `data`, `published`, `published_at`, `sort_order`. Rich text values inside `data` are **TipTap (ProseMirror) JSON**.
|
|
57
|
+
- **`UseEntryResult`** / **`UseEntriesResult`** – Return types of the hooks.
|
|
58
58
|
|
|
59
59
|
## Rendering content
|
|
60
60
|
|
|
@@ -78,26 +78,26 @@ function TipTapContent({ content }: { content: unknown }) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// In your post page:
|
|
81
|
-
<TipTapContent content={
|
|
81
|
+
<TipTapContent content={entry.data.body} />
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
## Example
|
|
85
85
|
|
|
86
86
|
```tsx
|
|
87
87
|
// List posts (e.g. homepage)
|
|
88
|
-
import {
|
|
88
|
+
import { useEntries } from "@dispatchcms/react";
|
|
89
89
|
|
|
90
90
|
export default function HomePage() {
|
|
91
|
-
const {
|
|
91
|
+
const { entries, isLoading, error } = useEntries("posts");
|
|
92
92
|
|
|
93
93
|
if (isLoading) return <p>Loading…</p>;
|
|
94
94
|
if (error) return <p>{error}</p>;
|
|
95
95
|
|
|
96
96
|
return (
|
|
97
97
|
<ul>
|
|
98
|
-
{
|
|
99
|
-
<li key={
|
|
100
|
-
<a href={`/blog/${
|
|
98
|
+
{entries.map((entry) => (
|
|
99
|
+
<li key={entry.id}>
|
|
100
|
+
<a href={`/blog/${entry.slug}`}>{entry.title}</a>
|
|
101
101
|
</li>
|
|
102
102
|
))}
|
|
103
103
|
</ul>
|
|
@@ -107,25 +107,35 @@ export default function HomePage() {
|
|
|
107
107
|
|
|
108
108
|
```tsx
|
|
109
109
|
// Single post page (e.g. /blog/:slug)
|
|
110
|
-
import {
|
|
110
|
+
import { useEntry } from "@dispatchcms/react";
|
|
111
111
|
import { TipTapContent } from "./TipTapContent"; // or inline renderToReactElement
|
|
112
112
|
|
|
113
113
|
export default function PostPage({ slug }) {
|
|
114
|
-
const {
|
|
114
|
+
const { entry, isLoading, error } = useEntry("posts", slug);
|
|
115
115
|
|
|
116
116
|
if (isLoading) return <p>Loading…</p>;
|
|
117
|
-
if (error || !
|
|
117
|
+
if (error || !entry) return <p>{error ?? "Not found"}</p>;
|
|
118
118
|
|
|
119
119
|
return (
|
|
120
120
|
<article>
|
|
121
|
-
<h1>{
|
|
122
|
-
|
|
123
|
-
<TipTapContent content={post.content} />
|
|
121
|
+
<h1>{entry.title}</h1>
|
|
122
|
+
<TipTapContent content={entry.data.body} />
|
|
124
123
|
</article>
|
|
125
124
|
);
|
|
126
125
|
}
|
|
127
126
|
```
|
|
128
127
|
|
|
128
|
+
## Images
|
|
129
|
+
|
|
130
|
+
An image field is a URL until an editor drags a point, then `{ url, focus }`. `imageProps` reads both. It only changes which part of the photo stays in view when your frame crops it.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { imageProps } from "@dispatchcms/react";
|
|
134
|
+
|
|
135
|
+
const photo = imageProps(entry.data.hero);
|
|
136
|
+
photo ? <img src={photo.src} alt="" className="h-full w-full" style={photo.style} /> : null
|
|
137
|
+
```
|
|
138
|
+
|
|
129
139
|
## License
|
|
130
140
|
|
|
131
141
|
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
type Entry = {
|
|
4
4
|
id: string;
|
|
5
5
|
created_at: string;
|
|
6
|
-
published_at: string;
|
|
6
|
+
published_at: string | null;
|
|
7
7
|
updated_at: string;
|
|
8
8
|
site_id: string;
|
|
9
|
+
collection: string;
|
|
9
10
|
title: string;
|
|
10
|
-
slug: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
featured_image: string | null;
|
|
11
|
+
slug: string | null;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
sort_order: number;
|
|
14
14
|
published: boolean;
|
|
15
15
|
};
|
|
16
16
|
|
|
@@ -26,17 +26,25 @@ type DispatchProviderProps = {
|
|
|
26
26
|
};
|
|
27
27
|
declare function DispatchProvider({ siteKey, apiBase, children, }: DispatchProviderProps): react_jsx_runtime.JSX.Element;
|
|
28
28
|
|
|
29
|
-
type
|
|
30
|
-
|
|
29
|
+
type UseEntryResult = {
|
|
30
|
+
entry: Entry | null;
|
|
31
31
|
isLoading: boolean;
|
|
32
32
|
error?: string;
|
|
33
33
|
};
|
|
34
|
-
declare function
|
|
35
|
-
type
|
|
36
|
-
|
|
34
|
+
declare function useEntry(collection: string | undefined, slug: string | undefined): UseEntryResult;
|
|
35
|
+
type UseEntriesResult = {
|
|
36
|
+
entries: Entry[];
|
|
37
37
|
isLoading: boolean;
|
|
38
38
|
error?: string;
|
|
39
39
|
};
|
|
40
|
-
declare function
|
|
40
|
+
declare function useEntries(collection: string | undefined): UseEntriesResult;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
declare function imageProps(value: unknown): {
|
|
43
|
+
src: string;
|
|
44
|
+
style: {
|
|
45
|
+
objectFit: "cover";
|
|
46
|
+
objectPosition: string;
|
|
47
|
+
};
|
|
48
|
+
} | null;
|
|
49
|
+
|
|
50
|
+
export { DispatchProvider, type Entry, type UseEntriesResult, type UseEntryResult, imageProps, useDispatchContext, useEntries, useEntry };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
type Entry = {
|
|
4
4
|
id: string;
|
|
5
5
|
created_at: string;
|
|
6
|
-
published_at: string;
|
|
6
|
+
published_at: string | null;
|
|
7
7
|
updated_at: string;
|
|
8
8
|
site_id: string;
|
|
9
|
+
collection: string;
|
|
9
10
|
title: string;
|
|
10
|
-
slug: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
featured_image: string | null;
|
|
11
|
+
slug: string | null;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
sort_order: number;
|
|
14
14
|
published: boolean;
|
|
15
15
|
};
|
|
16
16
|
|
|
@@ -26,17 +26,25 @@ type DispatchProviderProps = {
|
|
|
26
26
|
};
|
|
27
27
|
declare function DispatchProvider({ siteKey, apiBase, children, }: DispatchProviderProps): react_jsx_runtime.JSX.Element;
|
|
28
28
|
|
|
29
|
-
type
|
|
30
|
-
|
|
29
|
+
type UseEntryResult = {
|
|
30
|
+
entry: Entry | null;
|
|
31
31
|
isLoading: boolean;
|
|
32
32
|
error?: string;
|
|
33
33
|
};
|
|
34
|
-
declare function
|
|
35
|
-
type
|
|
36
|
-
|
|
34
|
+
declare function useEntry(collection: string | undefined, slug: string | undefined): UseEntryResult;
|
|
35
|
+
type UseEntriesResult = {
|
|
36
|
+
entries: Entry[];
|
|
37
37
|
isLoading: boolean;
|
|
38
38
|
error?: string;
|
|
39
39
|
};
|
|
40
|
-
declare function
|
|
40
|
+
declare function useEntries(collection: string | undefined): UseEntriesResult;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
declare function imageProps(value: unknown): {
|
|
43
|
+
src: string;
|
|
44
|
+
style: {
|
|
45
|
+
objectFit: "cover";
|
|
46
|
+
objectPosition: string;
|
|
47
|
+
};
|
|
48
|
+
} | null;
|
|
49
|
+
|
|
50
|
+
export { DispatchProvider, type Entry, type UseEntriesResult, type UseEntryResult, imageProps, useDispatchContext, useEntries, useEntry };
|
package/dist/index.js
CHANGED
|
@@ -22,9 +22,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
DispatchProvider: () => DispatchProvider,
|
|
25
|
+
imageProps: () => imageProps,
|
|
25
26
|
useDispatchContext: () => useDispatchContext,
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
useEntries: () => useEntries,
|
|
28
|
+
useEntry: () => useEntry
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(index_exports);
|
|
30
31
|
|
|
@@ -64,110 +65,140 @@ function DispatchProvider({
|
|
|
64
65
|
|
|
65
66
|
// src/hooks.ts
|
|
66
67
|
var import_react2 = require("react");
|
|
67
|
-
function
|
|
68
|
+
function useEntry(collection, slug) {
|
|
68
69
|
const ctx = useDispatchContext();
|
|
69
|
-
const [
|
|
70
|
+
const [entry, setEntry] = (0, import_react2.useState)(null);
|
|
70
71
|
const [isLoading, setIsLoading] = (0, import_react2.useState)(true);
|
|
71
72
|
const [error, setError] = (0, import_react2.useState)(void 0);
|
|
72
73
|
(0, import_react2.useEffect)(() => {
|
|
73
|
-
if (!slug?.trim()) {
|
|
74
|
-
|
|
74
|
+
if (!collection?.trim() || !slug?.trim()) {
|
|
75
|
+
setEntry(null);
|
|
75
76
|
setIsLoading(false);
|
|
76
77
|
setError(void 0);
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
80
|
if (!ctx?.siteKey) {
|
|
80
|
-
|
|
81
|
+
setEntry(null);
|
|
81
82
|
setIsLoading(false);
|
|
82
83
|
setError("Missing site key");
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
let cancelled = false;
|
|
86
|
-
|
|
87
|
+
setEntry(null);
|
|
87
88
|
setError(void 0);
|
|
88
89
|
setIsLoading(true);
|
|
89
|
-
const url = `${ctx.apiBase}/api/
|
|
90
|
+
const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}/${encodeURIComponent(slug.trim())}`;
|
|
90
91
|
fetch(url, {
|
|
91
92
|
headers: { "X-Site-Key": ctx.siteKey }
|
|
92
93
|
}).then((res) => {
|
|
93
94
|
if (cancelled) return;
|
|
94
95
|
if (res.status === 404) {
|
|
95
|
-
|
|
96
|
+
setEntry(null);
|
|
96
97
|
setIsLoading(false);
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
100
|
if (!res.ok) {
|
|
100
|
-
|
|
101
|
+
setEntry(null);
|
|
101
102
|
setIsLoading(false);
|
|
102
103
|
setError("Failed to load");
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
106
|
return res.json();
|
|
106
107
|
}).then((data) => {
|
|
107
|
-
if (cancelled) return;
|
|
108
|
-
|
|
108
|
+
if (cancelled || data == null) return;
|
|
109
|
+
setEntry(data);
|
|
109
110
|
setIsLoading(false);
|
|
110
111
|
}).catch(() => {
|
|
111
112
|
if (cancelled) return;
|
|
112
|
-
|
|
113
|
+
setEntry(null);
|
|
113
114
|
setIsLoading(false);
|
|
114
115
|
setError("Failed to load");
|
|
115
116
|
});
|
|
116
117
|
return () => {
|
|
117
118
|
cancelled = true;
|
|
118
119
|
};
|
|
119
|
-
}, [slug, ctx?.siteKey, ctx?.apiBase]);
|
|
120
|
-
return {
|
|
120
|
+
}, [collection, slug, ctx?.siteKey, ctx?.apiBase]);
|
|
121
|
+
return { entry, isLoading, error };
|
|
121
122
|
}
|
|
122
|
-
function
|
|
123
|
+
function useEntries(collection) {
|
|
123
124
|
const ctx = useDispatchContext();
|
|
124
|
-
const [
|
|
125
|
+
const [entries, setEntries] = (0, import_react2.useState)([]);
|
|
125
126
|
const [isLoading, setIsLoading] = (0, import_react2.useState)(true);
|
|
126
127
|
const [error, setError] = (0, import_react2.useState)(void 0);
|
|
127
128
|
(0, import_react2.useEffect)(() => {
|
|
129
|
+
if (!collection?.trim()) {
|
|
130
|
+
setEntries([]);
|
|
131
|
+
setIsLoading(false);
|
|
132
|
+
setError(void 0);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
128
135
|
if (!ctx?.siteKey) {
|
|
129
|
-
|
|
136
|
+
setEntries([]);
|
|
130
137
|
setIsLoading(false);
|
|
131
138
|
setError("Missing site key");
|
|
132
139
|
return;
|
|
133
140
|
}
|
|
134
141
|
let cancelled = false;
|
|
135
|
-
|
|
142
|
+
setEntries([]);
|
|
136
143
|
setError(void 0);
|
|
137
144
|
setIsLoading(true);
|
|
138
|
-
const url = `${ctx.apiBase}/api/
|
|
145
|
+
const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}`;
|
|
139
146
|
fetch(url, {
|
|
140
147
|
headers: { "X-Site-Key": ctx.siteKey }
|
|
141
148
|
}).then((res) => {
|
|
142
149
|
if (cancelled) return;
|
|
143
150
|
if (!res.ok) {
|
|
144
|
-
|
|
151
|
+
setEntries([]);
|
|
145
152
|
setIsLoading(false);
|
|
146
153
|
setError("Failed to load");
|
|
147
154
|
return;
|
|
148
155
|
}
|
|
149
156
|
return res.json();
|
|
150
157
|
}).then((data) => {
|
|
151
|
-
if (cancelled) return;
|
|
152
|
-
|
|
158
|
+
if (cancelled || data == null) return;
|
|
159
|
+
setEntries(Array.isArray(data) ? data : []);
|
|
153
160
|
setIsLoading(false);
|
|
154
161
|
}).catch(() => {
|
|
155
162
|
if (cancelled) return;
|
|
156
|
-
|
|
163
|
+
setEntries([]);
|
|
157
164
|
setIsLoading(false);
|
|
158
165
|
setError("Failed to load");
|
|
159
166
|
});
|
|
160
167
|
return () => {
|
|
161
168
|
cancelled = true;
|
|
162
169
|
};
|
|
163
|
-
}, [ctx?.siteKey, ctx?.apiBase]);
|
|
164
|
-
return {
|
|
170
|
+
}, [collection, ctx?.siteKey, ctx?.apiBase]);
|
|
171
|
+
return { entries, isLoading, error };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/index.ts
|
|
175
|
+
function imageProps(value) {
|
|
176
|
+
let src = "";
|
|
177
|
+
let x = 0.5;
|
|
178
|
+
let y = 0.5;
|
|
179
|
+
if (typeof value === "string") {
|
|
180
|
+
src = value;
|
|
181
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
182
|
+
const row = value;
|
|
183
|
+
if (typeof row.url !== "string") return null;
|
|
184
|
+
src = row.url;
|
|
185
|
+
if (row.focus && typeof row.focus === "object" && !Array.isArray(row.focus)) {
|
|
186
|
+
const focus = row.focus;
|
|
187
|
+
if (typeof focus.x === "number" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));
|
|
188
|
+
if (typeof focus.y === "number" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (!src) return null;
|
|
194
|
+
return { src, style: { objectFit: "cover", objectPosition: `${x * 100}% ${y * 100}%` } };
|
|
165
195
|
}
|
|
166
196
|
// Annotate the CommonJS export names for ESM import in node:
|
|
167
197
|
0 && (module.exports = {
|
|
168
198
|
DispatchProvider,
|
|
199
|
+
imageProps,
|
|
169
200
|
useDispatchContext,
|
|
170
|
-
|
|
171
|
-
|
|
201
|
+
useEntries,
|
|
202
|
+
useEntry
|
|
172
203
|
});
|
|
173
204
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/context.tsx","../src/config.ts","../src/hooks.ts"],"sourcesContent":["export type {
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.tsx","../src/config.ts","../src/hooks.ts"],"sourcesContent":["export type { Entry } from \"./types\";\nexport { DispatchProvider, useDispatchContext } from \"./context\";\nexport { useEntry, useEntries } from \"./hooks\";\nexport type { UseEntryResult, UseEntriesResult } from \"./hooks\";\n\n// ponytail: percentage object-position. Measure the image against the frame if a subject still clips.\nexport function imageProps(value: unknown): {\n src: string;\n style: { objectFit: \"cover\"; objectPosition: string };\n} | null {\n let src = \"\";\n let x = 0.5;\n let y = 0.5;\n if (typeof value === \"string\") {\n src = value;\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const row = value as Record<string, unknown>;\n if (typeof row.url !== \"string\") return null;\n src = row.url;\n if (row.focus && typeof row.focus === \"object\" && !Array.isArray(row.focus)) {\n const focus = row.focus as Record<string, unknown>;\n if (typeof focus.x === \"number\" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));\n if (typeof focus.y === \"number\" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));\n }\n } else {\n return null;\n }\n if (!src) return null;\n return { src, style: { objectFit: \"cover\", objectPosition: `${x * 100}% ${y * 100}%` } };\n}\n","\"use client\";\n\nimport React, { createContext, useContext } from \"react\";\nimport { getApiBase } from \"./config\";\n\ntype DispatchContextValue = {\n siteKey: string | null;\n apiBase: string;\n};\n\nconst DispatchContext = createContext<DispatchContextValue | null>(null);\n\nexport function useDispatchContext(): DispatchContextValue | null {\n return useContext(DispatchContext);\n}\n\ntype DispatchProviderProps = {\n siteKey?: string | null;\n apiBase?: string;\n children?: unknown;\n};\n\nexport function DispatchProvider({\n siteKey = null,\n apiBase = getApiBase(),\n children,\n}: DispatchProviderProps) {\n const value: DispatchContextValue = {\n siteKey: siteKey?.trim() || null,\n apiBase,\n };\n\n if (!value.siteKey) {\n if (typeof console !== \"undefined\" && console.warn) {\n console.warn(\n \"[Dispatch] No siteKey provided. Pass siteKey to DispatchProvider. Hooks will return empty/error state.\"\n );\n }\n }\n\n return (\n <DispatchContext.Provider value={value}>\n {children as React.ReactNode}\n </DispatchContext.Provider>\n );\n}\n","const DEFAULT_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport function getApiBase(): string {\n return DEFAULT_API_BASE;\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { Entry } from \"./types\";\nimport { useDispatchContext } from \"./context\";\n\nexport type UseEntryResult = {\n entry: Entry | null;\n isLoading: boolean;\n error?: string;\n};\n\nexport function useEntry(collection: string | undefined, slug: string | undefined): UseEntryResult {\n const ctx = useDispatchContext();\n const [entry, setEntry] = useState<Entry | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!collection?.trim() || !slug?.trim()) {\n setEntry(null);\n setIsLoading(false);\n setError(undefined);\n return;\n }\n\n if (!ctx?.siteKey) {\n setEntry(null);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setEntry(null);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}/${encodeURIComponent(slug.trim())}`;\n fetch(url, {\n headers: { \"X-Site-Key\": ctx.siteKey },\n })\n .then((res) => {\n if (cancelled) return;\n if (res.status === 404) {\n setEntry(null);\n setIsLoading(false);\n return;\n }\n if (!res.ok) {\n setEntry(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled || data == null) return;\n setEntry(data as Entry);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setEntry(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [collection, slug, ctx?.siteKey, ctx?.apiBase]);\n\n return { entry, isLoading, error };\n}\n\nexport type UseEntriesResult = {\n entries: Entry[];\n isLoading: boolean;\n error?: string;\n};\n\nexport function useEntries(collection: string | undefined): UseEntriesResult {\n const ctx = useDispatchContext();\n const [entries, setEntries] = useState<Entry[]>([]);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!collection?.trim()) {\n setEntries([]);\n setIsLoading(false);\n setError(undefined);\n return;\n }\n\n if (!ctx?.siteKey) {\n setEntries([]);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setEntries([]);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}`;\n fetch(url, {\n headers: { \"X-Site-Key\": ctx.siteKey },\n })\n .then((res) => {\n if (cancelled) return;\n if (!res.ok) {\n setEntries([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled || data == null) return;\n setEntries(Array.isArray(data) ? data : []);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setEntries([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [collection, ctx?.siteKey, ctx?.apiBase]);\n\n return { entries, isLoading, error };\n}\n"],"mappings":"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","names":["import_react"]}
|
package/dist/index.mjs
CHANGED
|
@@ -36,109 +36,139 @@ function DispatchProvider({
|
|
|
36
36
|
|
|
37
37
|
// src/hooks.ts
|
|
38
38
|
import { useEffect, useState } from "react";
|
|
39
|
-
function
|
|
39
|
+
function useEntry(collection, slug) {
|
|
40
40
|
const ctx = useDispatchContext();
|
|
41
|
-
const [
|
|
41
|
+
const [entry, setEntry] = useState(null);
|
|
42
42
|
const [isLoading, setIsLoading] = useState(true);
|
|
43
43
|
const [error, setError] = useState(void 0);
|
|
44
44
|
useEffect(() => {
|
|
45
|
-
if (!slug?.trim()) {
|
|
46
|
-
|
|
45
|
+
if (!collection?.trim() || !slug?.trim()) {
|
|
46
|
+
setEntry(null);
|
|
47
47
|
setIsLoading(false);
|
|
48
48
|
setError(void 0);
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
if (!ctx?.siteKey) {
|
|
52
|
-
|
|
52
|
+
setEntry(null);
|
|
53
53
|
setIsLoading(false);
|
|
54
54
|
setError("Missing site key");
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
let cancelled = false;
|
|
58
|
-
|
|
58
|
+
setEntry(null);
|
|
59
59
|
setError(void 0);
|
|
60
60
|
setIsLoading(true);
|
|
61
|
-
const url = `${ctx.apiBase}/api/
|
|
61
|
+
const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}/${encodeURIComponent(slug.trim())}`;
|
|
62
62
|
fetch(url, {
|
|
63
63
|
headers: { "X-Site-Key": ctx.siteKey }
|
|
64
64
|
}).then((res) => {
|
|
65
65
|
if (cancelled) return;
|
|
66
66
|
if (res.status === 404) {
|
|
67
|
-
|
|
67
|
+
setEntry(null);
|
|
68
68
|
setIsLoading(false);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
if (!res.ok) {
|
|
72
|
-
|
|
72
|
+
setEntry(null);
|
|
73
73
|
setIsLoading(false);
|
|
74
74
|
setError("Failed to load");
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
return res.json();
|
|
78
78
|
}).then((data) => {
|
|
79
|
-
if (cancelled) return;
|
|
80
|
-
|
|
79
|
+
if (cancelled || data == null) return;
|
|
80
|
+
setEntry(data);
|
|
81
81
|
setIsLoading(false);
|
|
82
82
|
}).catch(() => {
|
|
83
83
|
if (cancelled) return;
|
|
84
|
-
|
|
84
|
+
setEntry(null);
|
|
85
85
|
setIsLoading(false);
|
|
86
86
|
setError("Failed to load");
|
|
87
87
|
});
|
|
88
88
|
return () => {
|
|
89
89
|
cancelled = true;
|
|
90
90
|
};
|
|
91
|
-
}, [slug, ctx?.siteKey, ctx?.apiBase]);
|
|
92
|
-
return {
|
|
91
|
+
}, [collection, slug, ctx?.siteKey, ctx?.apiBase]);
|
|
92
|
+
return { entry, isLoading, error };
|
|
93
93
|
}
|
|
94
|
-
function
|
|
94
|
+
function useEntries(collection) {
|
|
95
95
|
const ctx = useDispatchContext();
|
|
96
|
-
const [
|
|
96
|
+
const [entries, setEntries] = useState([]);
|
|
97
97
|
const [isLoading, setIsLoading] = useState(true);
|
|
98
98
|
const [error, setError] = useState(void 0);
|
|
99
99
|
useEffect(() => {
|
|
100
|
+
if (!collection?.trim()) {
|
|
101
|
+
setEntries([]);
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
setError(void 0);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
100
106
|
if (!ctx?.siteKey) {
|
|
101
|
-
|
|
107
|
+
setEntries([]);
|
|
102
108
|
setIsLoading(false);
|
|
103
109
|
setError("Missing site key");
|
|
104
110
|
return;
|
|
105
111
|
}
|
|
106
112
|
let cancelled = false;
|
|
107
|
-
|
|
113
|
+
setEntries([]);
|
|
108
114
|
setError(void 0);
|
|
109
115
|
setIsLoading(true);
|
|
110
|
-
const url = `${ctx.apiBase}/api/
|
|
116
|
+
const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}`;
|
|
111
117
|
fetch(url, {
|
|
112
118
|
headers: { "X-Site-Key": ctx.siteKey }
|
|
113
119
|
}).then((res) => {
|
|
114
120
|
if (cancelled) return;
|
|
115
121
|
if (!res.ok) {
|
|
116
|
-
|
|
122
|
+
setEntries([]);
|
|
117
123
|
setIsLoading(false);
|
|
118
124
|
setError("Failed to load");
|
|
119
125
|
return;
|
|
120
126
|
}
|
|
121
127
|
return res.json();
|
|
122
128
|
}).then((data) => {
|
|
123
|
-
if (cancelled) return;
|
|
124
|
-
|
|
129
|
+
if (cancelled || data == null) return;
|
|
130
|
+
setEntries(Array.isArray(data) ? data : []);
|
|
125
131
|
setIsLoading(false);
|
|
126
132
|
}).catch(() => {
|
|
127
133
|
if (cancelled) return;
|
|
128
|
-
|
|
134
|
+
setEntries([]);
|
|
129
135
|
setIsLoading(false);
|
|
130
136
|
setError("Failed to load");
|
|
131
137
|
});
|
|
132
138
|
return () => {
|
|
133
139
|
cancelled = true;
|
|
134
140
|
};
|
|
135
|
-
}, [ctx?.siteKey, ctx?.apiBase]);
|
|
136
|
-
return {
|
|
141
|
+
}, [collection, ctx?.siteKey, ctx?.apiBase]);
|
|
142
|
+
return { entries, isLoading, error };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/index.ts
|
|
146
|
+
function imageProps(value) {
|
|
147
|
+
let src = "";
|
|
148
|
+
let x = 0.5;
|
|
149
|
+
let y = 0.5;
|
|
150
|
+
if (typeof value === "string") {
|
|
151
|
+
src = value;
|
|
152
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
153
|
+
const row = value;
|
|
154
|
+
if (typeof row.url !== "string") return null;
|
|
155
|
+
src = row.url;
|
|
156
|
+
if (row.focus && typeof row.focus === "object" && !Array.isArray(row.focus)) {
|
|
157
|
+
const focus = row.focus;
|
|
158
|
+
if (typeof focus.x === "number" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));
|
|
159
|
+
if (typeof focus.y === "number" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (!src) return null;
|
|
165
|
+
return { src, style: { objectFit: "cover", objectPosition: `${x * 100}% ${y * 100}%` } };
|
|
137
166
|
}
|
|
138
167
|
export {
|
|
139
168
|
DispatchProvider,
|
|
169
|
+
imageProps,
|
|
140
170
|
useDispatchContext,
|
|
141
|
-
|
|
142
|
-
|
|
171
|
+
useEntries,
|
|
172
|
+
useEntry
|
|
143
173
|
};
|
|
144
174
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/context.tsx","../src/config.ts","../src/hooks.ts"],"sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext } from \"react\";\nimport { getApiBase } from \"./config\";\n\ntype DispatchContextValue = {\n siteKey: string | null;\n apiBase: string;\n};\n\nconst DispatchContext = createContext<DispatchContextValue | null>(null);\n\nexport function useDispatchContext(): DispatchContextValue | null {\n return useContext(DispatchContext);\n}\n\ntype DispatchProviderProps = {\n siteKey?: string | null;\n apiBase?: string;\n children?: unknown;\n};\n\nexport function DispatchProvider({\n siteKey = null,\n apiBase = getApiBase(),\n children,\n}: DispatchProviderProps) {\n const value: DispatchContextValue = {\n siteKey: siteKey?.trim() || null,\n apiBase,\n };\n\n if (!value.siteKey) {\n if (typeof console !== \"undefined\" && console.warn) {\n console.warn(\n \"[Dispatch] No siteKey provided. Pass siteKey to DispatchProvider. Hooks will return empty/error state.\"\n );\n }\n }\n\n return (\n <DispatchContext.Provider value={value}>\n {children as React.ReactNode}\n </DispatchContext.Provider>\n );\n}\n","const DEFAULT_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport function getApiBase(): string {\n return DEFAULT_API_BASE;\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type {
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/config.ts","../src/hooks.ts","../src/index.ts"],"sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext } from \"react\";\nimport { getApiBase } from \"./config\";\n\ntype DispatchContextValue = {\n siteKey: string | null;\n apiBase: string;\n};\n\nconst DispatchContext = createContext<DispatchContextValue | null>(null);\n\nexport function useDispatchContext(): DispatchContextValue | null {\n return useContext(DispatchContext);\n}\n\ntype DispatchProviderProps = {\n siteKey?: string | null;\n apiBase?: string;\n children?: unknown;\n};\n\nexport function DispatchProvider({\n siteKey = null,\n apiBase = getApiBase(),\n children,\n}: DispatchProviderProps) {\n const value: DispatchContextValue = {\n siteKey: siteKey?.trim() || null,\n apiBase,\n };\n\n if (!value.siteKey) {\n if (typeof console !== \"undefined\" && console.warn) {\n console.warn(\n \"[Dispatch] No siteKey provided. Pass siteKey to DispatchProvider. Hooks will return empty/error state.\"\n );\n }\n }\n\n return (\n <DispatchContext.Provider value={value}>\n {children as React.ReactNode}\n </DispatchContext.Provider>\n );\n}\n","const DEFAULT_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport function getApiBase(): string {\n return DEFAULT_API_BASE;\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { Entry } from \"./types\";\nimport { useDispatchContext } from \"./context\";\n\nexport type UseEntryResult = {\n entry: Entry | null;\n isLoading: boolean;\n error?: string;\n};\n\nexport function useEntry(collection: string | undefined, slug: string | undefined): UseEntryResult {\n const ctx = useDispatchContext();\n const [entry, setEntry] = useState<Entry | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!collection?.trim() || !slug?.trim()) {\n setEntry(null);\n setIsLoading(false);\n setError(undefined);\n return;\n }\n\n if (!ctx?.siteKey) {\n setEntry(null);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setEntry(null);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}/${encodeURIComponent(slug.trim())}`;\n fetch(url, {\n headers: { \"X-Site-Key\": ctx.siteKey },\n })\n .then((res) => {\n if (cancelled) return;\n if (res.status === 404) {\n setEntry(null);\n setIsLoading(false);\n return;\n }\n if (!res.ok) {\n setEntry(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled || data == null) return;\n setEntry(data as Entry);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setEntry(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [collection, slug, ctx?.siteKey, ctx?.apiBase]);\n\n return { entry, isLoading, error };\n}\n\nexport type UseEntriesResult = {\n entries: Entry[];\n isLoading: boolean;\n error?: string;\n};\n\nexport function useEntries(collection: string | undefined): UseEntriesResult {\n const ctx = useDispatchContext();\n const [entries, setEntries] = useState<Entry[]>([]);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!collection?.trim()) {\n setEntries([]);\n setIsLoading(false);\n setError(undefined);\n return;\n }\n\n if (!ctx?.siteKey) {\n setEntries([]);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setEntries([]);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}`;\n fetch(url, {\n headers: { \"X-Site-Key\": ctx.siteKey },\n })\n .then((res) => {\n if (cancelled) return;\n if (!res.ok) {\n setEntries([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled || data == null) return;\n setEntries(Array.isArray(data) ? data : []);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setEntries([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [collection, ctx?.siteKey, ctx?.apiBase]);\n\n return { entries, isLoading, error };\n}\n","export type { Entry } from \"./types\";\nexport { DispatchProvider, useDispatchContext } from \"./context\";\nexport { useEntry, useEntries } from \"./hooks\";\nexport type { UseEntryResult, UseEntriesResult } from \"./hooks\";\n\n// ponytail: percentage object-position. Measure the image against the frame if a subject still clips.\nexport function imageProps(value: unknown): {\n src: string;\n style: { objectFit: \"cover\"; objectPosition: string };\n} | null {\n let src = \"\";\n let x = 0.5;\n let y = 0.5;\n if (typeof value === \"string\") {\n src = value;\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n const row = value as Record<string, unknown>;\n if (typeof row.url !== \"string\") return null;\n src = row.url;\n if (row.focus && typeof row.focus === \"object\" && !Array.isArray(row.focus)) {\n const focus = row.focus as Record<string, unknown>;\n if (typeof focus.x === \"number\" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));\n if (typeof focus.y === \"number\" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));\n }\n } else {\n return null;\n }\n if (!src) return null;\n return { src, style: { objectFit: \"cover\", objectPosition: `${x * 100}% ${y * 100}%` } };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dispatchcms/react",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "React hooks and provider for Dispatch CMS",
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "React hooks and provider for Dispatch CMS entries",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
package/src/hooks.ts
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import type {
|
|
4
|
+
import type { Entry } from "./types";
|
|
5
5
|
import { useDispatchContext } from "./context";
|
|
6
6
|
|
|
7
|
-
export type
|
|
8
|
-
|
|
7
|
+
export type UseEntryResult = {
|
|
8
|
+
entry: Entry | null;
|
|
9
9
|
isLoading: boolean;
|
|
10
10
|
error?: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export function
|
|
13
|
+
export function useEntry(collection: string | undefined, slug: string | undefined): UseEntryResult {
|
|
14
14
|
const ctx = useDispatchContext();
|
|
15
|
-
const [
|
|
15
|
+
const [entry, setEntry] = useState<Entry | null>(null);
|
|
16
16
|
const [isLoading, setIsLoading] = useState(true);
|
|
17
17
|
const [error, setError] = useState<string | undefined>(undefined);
|
|
18
18
|
|
|
19
19
|
useEffect(() => {
|
|
20
|
-
if (!slug?.trim()) {
|
|
21
|
-
|
|
20
|
+
if (!collection?.trim() || !slug?.trim()) {
|
|
21
|
+
setEntry(null);
|
|
22
22
|
setIsLoading(false);
|
|
23
23
|
setError(undefined);
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (!ctx?.siteKey) {
|
|
28
|
-
|
|
28
|
+
setEntry(null);
|
|
29
29
|
setIsLoading(false);
|
|
30
30
|
setError("Missing site key");
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
let cancelled = false;
|
|
35
|
-
|
|
35
|
+
setEntry(null);
|
|
36
36
|
setError(undefined);
|
|
37
37
|
setIsLoading(true);
|
|
38
38
|
|
|
39
|
-
const url = `${ctx.apiBase}/api/
|
|
39
|
+
const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}/${encodeURIComponent(slug.trim())}`;
|
|
40
40
|
fetch(url, {
|
|
41
41
|
headers: { "X-Site-Key": ctx.siteKey },
|
|
42
42
|
})
|
|
43
43
|
.then((res) => {
|
|
44
44
|
if (cancelled) return;
|
|
45
45
|
if (res.status === 404) {
|
|
46
|
-
|
|
46
|
+
setEntry(null);
|
|
47
47
|
setIsLoading(false);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
if (!res.ok) {
|
|
51
|
-
|
|
51
|
+
setEntry(null);
|
|
52
52
|
setIsLoading(false);
|
|
53
53
|
setError("Failed to load");
|
|
54
54
|
return;
|
|
@@ -56,13 +56,13 @@ export function usePost(slug: string | undefined): UsePostResult {
|
|
|
56
56
|
return res.json();
|
|
57
57
|
})
|
|
58
58
|
.then((data) => {
|
|
59
|
-
if (cancelled) return;
|
|
60
|
-
|
|
59
|
+
if (cancelled || data == null) return;
|
|
60
|
+
setEntry(data as Entry);
|
|
61
61
|
setIsLoading(false);
|
|
62
62
|
})
|
|
63
63
|
.catch(() => {
|
|
64
64
|
if (cancelled) return;
|
|
65
|
-
|
|
65
|
+
setEntry(null);
|
|
66
66
|
setIsLoading(false);
|
|
67
67
|
setError("Failed to load");
|
|
68
68
|
});
|
|
@@ -70,44 +70,51 @@ export function usePost(slug: string | undefined): UsePostResult {
|
|
|
70
70
|
return () => {
|
|
71
71
|
cancelled = true;
|
|
72
72
|
};
|
|
73
|
-
}, [slug, ctx?.siteKey, ctx?.apiBase]);
|
|
73
|
+
}, [collection, slug, ctx?.siteKey, ctx?.apiBase]);
|
|
74
74
|
|
|
75
|
-
return {
|
|
75
|
+
return { entry, isLoading, error };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export type
|
|
79
|
-
|
|
78
|
+
export type UseEntriesResult = {
|
|
79
|
+
entries: Entry[];
|
|
80
80
|
isLoading: boolean;
|
|
81
81
|
error?: string;
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
export function
|
|
84
|
+
export function useEntries(collection: string | undefined): UseEntriesResult {
|
|
85
85
|
const ctx = useDispatchContext();
|
|
86
|
-
const [
|
|
86
|
+
const [entries, setEntries] = useState<Entry[]>([]);
|
|
87
87
|
const [isLoading, setIsLoading] = useState(true);
|
|
88
88
|
const [error, setError] = useState<string | undefined>(undefined);
|
|
89
89
|
|
|
90
90
|
useEffect(() => {
|
|
91
|
+
if (!collection?.trim()) {
|
|
92
|
+
setEntries([]);
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
setError(undefined);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
if (!ctx?.siteKey) {
|
|
92
|
-
|
|
99
|
+
setEntries([]);
|
|
93
100
|
setIsLoading(false);
|
|
94
101
|
setError("Missing site key");
|
|
95
102
|
return;
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
let cancelled = false;
|
|
99
|
-
|
|
106
|
+
setEntries([]);
|
|
100
107
|
setError(undefined);
|
|
101
108
|
setIsLoading(true);
|
|
102
109
|
|
|
103
|
-
const url = `${ctx.apiBase}/api/
|
|
110
|
+
const url = `${ctx.apiBase}/api/content/${encodeURIComponent(collection.trim())}`;
|
|
104
111
|
fetch(url, {
|
|
105
112
|
headers: { "X-Site-Key": ctx.siteKey },
|
|
106
113
|
})
|
|
107
114
|
.then((res) => {
|
|
108
115
|
if (cancelled) return;
|
|
109
116
|
if (!res.ok) {
|
|
110
|
-
|
|
117
|
+
setEntries([]);
|
|
111
118
|
setIsLoading(false);
|
|
112
119
|
setError("Failed to load");
|
|
113
120
|
return;
|
|
@@ -115,13 +122,13 @@ export function usePosts(): UsePostsResult {
|
|
|
115
122
|
return res.json();
|
|
116
123
|
})
|
|
117
124
|
.then((data) => {
|
|
118
|
-
if (cancelled) return;
|
|
119
|
-
|
|
125
|
+
if (cancelled || data == null) return;
|
|
126
|
+
setEntries(Array.isArray(data) ? data : []);
|
|
120
127
|
setIsLoading(false);
|
|
121
128
|
})
|
|
122
129
|
.catch(() => {
|
|
123
130
|
if (cancelled) return;
|
|
124
|
-
|
|
131
|
+
setEntries([]);
|
|
125
132
|
setIsLoading(false);
|
|
126
133
|
setError("Failed to load");
|
|
127
134
|
});
|
|
@@ -129,7 +136,7 @@ export function usePosts(): UsePostsResult {
|
|
|
129
136
|
return () => {
|
|
130
137
|
cancelled = true;
|
|
131
138
|
};
|
|
132
|
-
}, [ctx?.siteKey, ctx?.apiBase]);
|
|
139
|
+
}, [collection, ctx?.siteKey, ctx?.apiBase]);
|
|
133
140
|
|
|
134
|
-
return {
|
|
141
|
+
return { entries, isLoading, error };
|
|
135
142
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { Entry } from "./types";
|
|
2
2
|
export { DispatchProvider, useDispatchContext } from "./context";
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { useEntry, useEntries } from "./hooks";
|
|
4
|
+
export type { UseEntryResult, UseEntriesResult } from "./hooks";
|
|
5
|
+
|
|
6
|
+
// ponytail: percentage object-position. Measure the image against the frame if a subject still clips.
|
|
7
|
+
export function imageProps(value: unknown): {
|
|
8
|
+
src: string;
|
|
9
|
+
style: { objectFit: "cover"; objectPosition: string };
|
|
10
|
+
} | null {
|
|
11
|
+
let src = "";
|
|
12
|
+
let x = 0.5;
|
|
13
|
+
let y = 0.5;
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
src = value;
|
|
16
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
17
|
+
const row = value as Record<string, unknown>;
|
|
18
|
+
if (typeof row.url !== "string") return null;
|
|
19
|
+
src = row.url;
|
|
20
|
+
if (row.focus && typeof row.focus === "object" && !Array.isArray(row.focus)) {
|
|
21
|
+
const focus = row.focus as Record<string, unknown>;
|
|
22
|
+
if (typeof focus.x === "number" && Number.isFinite(focus.x)) x = Math.min(1, Math.max(0, focus.x));
|
|
23
|
+
if (typeof focus.y === "number" && Number.isFinite(focus.y)) y = Math.min(1, Math.max(0, focus.y));
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (!src) return null;
|
|
29
|
+
return { src, style: { objectFit: "cover", objectPosition: `${x * 100}% ${y * 100}%` } };
|
|
30
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type Entry = {
|
|
2
2
|
id: string;
|
|
3
3
|
created_at: string;
|
|
4
|
-
published_at: string;
|
|
4
|
+
published_at: string | null;
|
|
5
5
|
updated_at: string;
|
|
6
6
|
site_id: string;
|
|
7
|
+
collection: string;
|
|
7
8
|
title: string;
|
|
8
|
-
slug: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
featured_image: string | null;
|
|
9
|
+
slug: string | null;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
sort_order: number;
|
|
12
12
|
published: boolean;
|
|
13
13
|
};
|