@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 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 `usePost` / `usePosts` to fetch published posts in any React app (Next.js, Vite, etc.).
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
- ### `usePost(slug)`
35
+ ### `useEntry(collection, slug)`
36
36
 
37
- Fetches a single published post by slug.
37
+ Fetches a single published entry.
38
38
 
39
- - **Returns:** `{ post: Post | null, isLoading: boolean, error?: string }`
40
- - On failure or missing site key, does not throw; returns `post: null`, `isLoading: false`, and `error` set.
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
- ### `usePosts()`
42
+ ### `useEntries(collection)`
43
43
 
44
- Fetches all published posts for the site.
44
+ Fetches published entries for a collection.
45
45
 
46
- - **Returns:** `{ posts: Post[], isLoading: boolean, error?: string }`
47
- - On failure or missing site key, does not throw; returns `posts: []`, `isLoading: false`, and `error` set.
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 `post: null` / `posts: []`, `isLoading: false`, and `error: "Failed to load"`. The React tree is never crashed.
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
- - **`Post`** – Post from the CMS. Fields include `id`, `title`, `slug`, `content`, `excerpt`, `featured_image`, `published`, `published_at`, `created_at`, `updated_at`, `site_id`. The `content` field is **TipTap (ProseMirror) JSON**.
57
- - **`UsePostResult`** / **`UsePostsResult`** – Return types of the hooks.
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={post.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 { usePosts } from "@dispatchcms/react";
88
+ import { useEntries } from "@dispatchcms/react";
89
89
 
90
90
  export default function HomePage() {
91
- const { posts, isLoading, error } = usePosts();
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
- {posts.map((post) => (
99
- <li key={post.id}>
100
- <a href={`/blog/${post.slug}`}>{post.title}</a>
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 { usePost } from "@dispatchcms/react";
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 { post, isLoading, error } = usePost(slug);
114
+ const { entry, isLoading, error } = useEntry("posts", slug);
115
115
 
116
116
  if (isLoading) return <p>Loading…</p>;
117
- if (error || !post) return <p>{error ?? "Not found"}</p>;
117
+ if (error || !entry) return <p>{error ?? "Not found"}</p>;
118
118
 
119
119
  return (
120
120
  <article>
121
- <h1>{post.title}</h1>
122
- {post.excerpt && <p>{post.excerpt}</p>}
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 Post = {
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
- content: unknown;
12
- excerpt: string | null;
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 UsePostResult = {
30
- post: Post | null;
29
+ type UseEntryResult = {
30
+ entry: Entry | null;
31
31
  isLoading: boolean;
32
32
  error?: string;
33
33
  };
34
- declare function usePost(slug: string | undefined): UsePostResult;
35
- type UsePostsResult = {
36
- posts: Post[];
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 usePosts(): UsePostsResult;
40
+ declare function useEntries(collection: string | undefined): UseEntriesResult;
41
41
 
42
- export { DispatchProvider, type Post, type UsePostResult, type UsePostsResult, useDispatchContext, usePost, usePosts };
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 Post = {
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
- content: unknown;
12
- excerpt: string | null;
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 UsePostResult = {
30
- post: Post | null;
29
+ type UseEntryResult = {
30
+ entry: Entry | null;
31
31
  isLoading: boolean;
32
32
  error?: string;
33
33
  };
34
- declare function usePost(slug: string | undefined): UsePostResult;
35
- type UsePostsResult = {
36
- posts: Post[];
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 usePosts(): UsePostsResult;
40
+ declare function useEntries(collection: string | undefined): UseEntriesResult;
41
41
 
42
- export { DispatchProvider, type Post, type UsePostResult, type UsePostsResult, useDispatchContext, usePost, usePosts };
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
- usePost: () => usePost,
27
- usePosts: () => usePosts
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 usePost(slug) {
68
+ function useEntry(collection, slug) {
68
69
  const ctx = useDispatchContext();
69
- const [post, setPost] = (0, import_react2.useState)(null);
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
- setPost(null);
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
- setPost(null);
81
+ setEntry(null);
81
82
  setIsLoading(false);
82
83
  setError("Missing site key");
83
84
  return;
84
85
  }
85
86
  let cancelled = false;
86
- setPost(null);
87
+ setEntry(null);
87
88
  setError(void 0);
88
89
  setIsLoading(true);
89
- const url = `${ctx.apiBase}/api/posts/${encodeURIComponent(slug.trim())}`;
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
- setPost(null);
96
+ setEntry(null);
96
97
  setIsLoading(false);
97
98
  return;
98
99
  }
99
100
  if (!res.ok) {
100
- setPost(null);
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
- setPost(data);
108
+ if (cancelled || data == null) return;
109
+ setEntry(data);
109
110
  setIsLoading(false);
110
111
  }).catch(() => {
111
112
  if (cancelled) return;
112
- setPost(null);
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 { post, isLoading, error };
120
+ }, [collection, slug, ctx?.siteKey, ctx?.apiBase]);
121
+ return { entry, isLoading, error };
121
122
  }
122
- function usePosts() {
123
+ function useEntries(collection) {
123
124
  const ctx = useDispatchContext();
124
- const [posts, setPosts] = (0, import_react2.useState)([]);
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
- setPosts([]);
136
+ setEntries([]);
130
137
  setIsLoading(false);
131
138
  setError("Missing site key");
132
139
  return;
133
140
  }
134
141
  let cancelled = false;
135
- setPosts([]);
142
+ setEntries([]);
136
143
  setError(void 0);
137
144
  setIsLoading(true);
138
- const url = `${ctx.apiBase}/api/posts`;
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
- setPosts([]);
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
- setPosts(Array.isArray(data) ? data : []);
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
- setPosts([]);
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 { posts, isLoading, error };
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
- usePost,
171
- usePosts
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 { Post } from \"./types\";\nexport { DispatchProvider, useDispatchContext } from \"./context\";\nexport { usePost, usePosts } from \"./hooks\";\nexport type { UsePostResult, UsePostsResult } from \"./hooks\";\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 { Post } from \"./types\";\nimport { useDispatchContext } from \"./context\";\n\nexport type UsePostResult = {\n post: Post | null;\n isLoading: boolean;\n error?: string;\n};\n\nexport function usePost(slug: string | undefined): UsePostResult {\n const ctx = useDispatchContext();\n const [post, setPost] = useState<Post | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!slug?.trim()) {\n setPost(null);\n setIsLoading(false);\n setError(undefined);\n return;\n }\n\n if (!ctx?.siteKey) {\n setPost(null);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setPost(null);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/posts/${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 setPost(null);\n setIsLoading(false);\n return;\n }\n if (!res.ok) {\n setPost(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled) return;\n setPost(data as Post);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setPost(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [slug, ctx?.siteKey, ctx?.apiBase]);\n\n return { post, isLoading, error };\n}\n\nexport type UsePostsResult = {\n posts: Post[];\n isLoading: boolean;\n error?: string;\n};\n\nexport function usePosts(): UsePostsResult {\n const ctx = useDispatchContext();\n const [posts, setPosts] = useState<Post[]>([]);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!ctx?.siteKey) {\n setPosts([]);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setPosts([]);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/posts`;\n fetch(url, {\n headers: { \"X-Site-Key\": ctx.siteKey },\n })\n .then((res) => {\n if (cancelled) return;\n if (!res.ok) {\n setPosts([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled) return;\n setPosts(Array.isArray(data) ? data : []);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setPosts([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [ctx?.siteKey, ctx?.apiBase]);\n\n return { posts, isLoading, error };\n}\n"],"mappings":"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","names":["import_react"]}
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 usePost(slug) {
39
+ function useEntry(collection, slug) {
40
40
  const ctx = useDispatchContext();
41
- const [post, setPost] = useState(null);
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
- setPost(null);
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
- setPost(null);
52
+ setEntry(null);
53
53
  setIsLoading(false);
54
54
  setError("Missing site key");
55
55
  return;
56
56
  }
57
57
  let cancelled = false;
58
- setPost(null);
58
+ setEntry(null);
59
59
  setError(void 0);
60
60
  setIsLoading(true);
61
- const url = `${ctx.apiBase}/api/posts/${encodeURIComponent(slug.trim())}`;
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
- setPost(null);
67
+ setEntry(null);
68
68
  setIsLoading(false);
69
69
  return;
70
70
  }
71
71
  if (!res.ok) {
72
- setPost(null);
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
- setPost(data);
79
+ if (cancelled || data == null) return;
80
+ setEntry(data);
81
81
  setIsLoading(false);
82
82
  }).catch(() => {
83
83
  if (cancelled) return;
84
- setPost(null);
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 { post, isLoading, error };
91
+ }, [collection, slug, ctx?.siteKey, ctx?.apiBase]);
92
+ return { entry, isLoading, error };
93
93
  }
94
- function usePosts() {
94
+ function useEntries(collection) {
95
95
  const ctx = useDispatchContext();
96
- const [posts, setPosts] = useState([]);
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
- setPosts([]);
107
+ setEntries([]);
102
108
  setIsLoading(false);
103
109
  setError("Missing site key");
104
110
  return;
105
111
  }
106
112
  let cancelled = false;
107
- setPosts([]);
113
+ setEntries([]);
108
114
  setError(void 0);
109
115
  setIsLoading(true);
110
- const url = `${ctx.apiBase}/api/posts`;
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
- setPosts([]);
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
- setPosts(Array.isArray(data) ? data : []);
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
- setPosts([]);
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 { posts, isLoading, error };
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
- usePost,
142
- usePosts
171
+ useEntries,
172
+ useEntry
143
173
  };
144
174
  //# sourceMappingURL=index.mjs.map
@@ -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 { Post } from \"./types\";\nimport { useDispatchContext } from \"./context\";\n\nexport type UsePostResult = {\n post: Post | null;\n isLoading: boolean;\n error?: string;\n};\n\nexport function usePost(slug: string | undefined): UsePostResult {\n const ctx = useDispatchContext();\n const [post, setPost] = useState<Post | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!slug?.trim()) {\n setPost(null);\n setIsLoading(false);\n setError(undefined);\n return;\n }\n\n if (!ctx?.siteKey) {\n setPost(null);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setPost(null);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/posts/${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 setPost(null);\n setIsLoading(false);\n return;\n }\n if (!res.ok) {\n setPost(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled) return;\n setPost(data as Post);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setPost(null);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [slug, ctx?.siteKey, ctx?.apiBase]);\n\n return { post, isLoading, error };\n}\n\nexport type UsePostsResult = {\n posts: Post[];\n isLoading: boolean;\n error?: string;\n};\n\nexport function usePosts(): UsePostsResult {\n const ctx = useDispatchContext();\n const [posts, setPosts] = useState<Post[]>([]);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | undefined>(undefined);\n\n useEffect(() => {\n if (!ctx?.siteKey) {\n setPosts([]);\n setIsLoading(false);\n setError(\"Missing site key\");\n return;\n }\n\n let cancelled = false;\n setPosts([]);\n setError(undefined);\n setIsLoading(true);\n\n const url = `${ctx.apiBase}/api/posts`;\n fetch(url, {\n headers: { \"X-Site-Key\": ctx.siteKey },\n })\n .then((res) => {\n if (cancelled) return;\n if (!res.ok) {\n setPosts([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n return;\n }\n return res.json();\n })\n .then((data) => {\n if (cancelled) return;\n setPosts(Array.isArray(data) ? data : []);\n setIsLoading(false);\n })\n .catch(() => {\n if (cancelled) return;\n setPosts([]);\n setIsLoading(false);\n setError(\"Failed to load\");\n });\n\n return () => {\n cancelled = true;\n };\n }, [ctx?.siteKey, ctx?.apiBase]);\n\n return { posts, isLoading, error };\n}\n"],"mappings":"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","names":[]}
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.6",
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 { Post } from "./types";
4
+ import type { Entry } from "./types";
5
5
  import { useDispatchContext } from "./context";
6
6
 
7
- export type UsePostResult = {
8
- post: Post | null;
7
+ export type UseEntryResult = {
8
+ entry: Entry | null;
9
9
  isLoading: boolean;
10
10
  error?: string;
11
11
  };
12
12
 
13
- export function usePost(slug: string | undefined): UsePostResult {
13
+ export function useEntry(collection: string | undefined, slug: string | undefined): UseEntryResult {
14
14
  const ctx = useDispatchContext();
15
- const [post, setPost] = useState<Post | null>(null);
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
- setPost(null);
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
- setPost(null);
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
- setPost(null);
35
+ setEntry(null);
36
36
  setError(undefined);
37
37
  setIsLoading(true);
38
38
 
39
- const url = `${ctx.apiBase}/api/posts/${encodeURIComponent(slug.trim())}`;
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
- setPost(null);
46
+ setEntry(null);
47
47
  setIsLoading(false);
48
48
  return;
49
49
  }
50
50
  if (!res.ok) {
51
- setPost(null);
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
- setPost(data as Post);
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
- setPost(null);
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 { post, isLoading, error };
75
+ return { entry, isLoading, error };
76
76
  }
77
77
 
78
- export type UsePostsResult = {
79
- posts: Post[];
78
+ export type UseEntriesResult = {
79
+ entries: Entry[];
80
80
  isLoading: boolean;
81
81
  error?: string;
82
82
  };
83
83
 
84
- export function usePosts(): UsePostsResult {
84
+ export function useEntries(collection: string | undefined): UseEntriesResult {
85
85
  const ctx = useDispatchContext();
86
- const [posts, setPosts] = useState<Post[]>([]);
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
- setPosts([]);
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
- setPosts([]);
106
+ setEntries([]);
100
107
  setError(undefined);
101
108
  setIsLoading(true);
102
109
 
103
- const url = `${ctx.apiBase}/api/posts`;
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
- setPosts([]);
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
- setPosts(Array.isArray(data) ? data : []);
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
- setPosts([]);
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 { posts, isLoading, error };
141
+ return { entries, isLoading, error };
135
142
  }
package/src/index.ts CHANGED
@@ -1,4 +1,30 @@
1
- export type { Post } from "./types";
1
+ export type { Entry } from "./types";
2
2
  export { DispatchProvider, useDispatchContext } from "./context";
3
- export { usePost, usePosts } from "./hooks";
4
- export type { UsePostResult, UsePostsResult } from "./hooks";
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 Post = {
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
- content: unknown;
10
- excerpt: string | null;
11
- featured_image: string | null;
9
+ slug: string | null;
10
+ data: Record<string, unknown>;
11
+ sort_order: number;
12
12
  published: boolean;
13
13
  };