@dispatchcms/next 0.0.7 → 0.0.9

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
@@ -148,3 +148,18 @@ export default async function PreviewPage({
148
148
  ```
149
149
 
150
150
  In the CMS, set **Site URL** in Site settings to your site’s base URL (e.g. `https://yoursite.com`). The preview link will use that URL plus `/preview?token=...`.
151
+
152
+ ## Images
153
+
154
+ An image field is a URL until an editor crops it, then `{ url, crop }` with a unit rectangle (`x`, `y`, `width`, `height` in `0…1`). The original file is unchanged. Wrap the image in a sized `overflow-hidden` frame and spread `imageProps` onto the `<img>`:
155
+
156
+ ```tsx
157
+ import { imageProps } from "@dispatchcms/next";
158
+
159
+ const photo = imageProps(entry.data.hero);
160
+ photo ? (
161
+ <div className="relative h-64 w-full overflow-hidden">
162
+ <img src={photo.src} alt="" style={photo.style} />
163
+ </div>
164
+ ) : null;
165
+ ```
package/dist/index.d.mts CHANGED
@@ -22,5 +22,25 @@ declare function getEntries(collection: string, siteKey?: string): Promise<Entry
22
22
  declare function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null>;
23
23
  /** Draft or published entry. The token is the secret; no site key is required. */
24
24
  declare function getEntryByPreviewToken(token: string): Promise<Entry | null>;
25
+ type ImageCrop = {
26
+ x: number;
27
+ y: number;
28
+ width: number;
29
+ height: number;
30
+ };
31
+ /** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */
32
+ declare function imageProps(value: unknown): {
33
+ src: string;
34
+ crop: ImageCrop;
35
+ style: {
36
+ position: "absolute";
37
+ left: string;
38
+ top: string;
39
+ width: string;
40
+ height: string;
41
+ maxWidth: "none";
42
+ maxHeight: "none";
43
+ };
44
+ } | null;
25
45
 
26
- export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, initDispatch };
46
+ export { type DispatchConfig, type Entry, type ImageCrop, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
package/dist/index.d.ts CHANGED
@@ -22,5 +22,25 @@ declare function getEntries(collection: string, siteKey?: string): Promise<Entry
22
22
  declare function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null>;
23
23
  /** Draft or published entry. The token is the secret; no site key is required. */
24
24
  declare function getEntryByPreviewToken(token: string): Promise<Entry | null>;
25
+ type ImageCrop = {
26
+ x: number;
27
+ y: number;
28
+ width: number;
29
+ height: number;
30
+ };
31
+ /** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */
32
+ declare function imageProps(value: unknown): {
33
+ src: string;
34
+ crop: ImageCrop;
35
+ style: {
36
+ position: "absolute";
37
+ left: string;
38
+ top: string;
39
+ width: string;
40
+ height: string;
41
+ maxWidth: "none";
42
+ maxHeight: "none";
43
+ };
44
+ } | null;
25
45
 
26
- export { type DispatchConfig, type Entry, getConfig, getEntries, getEntry, getEntryByPreviewToken, initDispatch };
46
+ export { type DispatchConfig, type Entry, type ImageCrop, getConfig, getEntries, getEntry, getEntryByPreviewToken, imageProps, initDispatch };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  getEntries: () => getEntries,
25
25
  getEntry: () => getEntry,
26
26
  getEntryByPreviewToken: () => getEntryByPreviewToken,
27
+ imageProps: () => imageProps,
27
28
  initDispatch: () => initDispatch
28
29
  });
29
30
  module.exports = __toCommonJS(index_exports);
@@ -85,12 +86,57 @@ async function getEntryByPreviewToken(token) {
85
86
  }
86
87
  return await res.json();
87
88
  }
89
+ function clampCropSide(n, fallback) {
90
+ return typeof n === "number" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;
91
+ }
92
+ function readCrop(value) {
93
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
94
+ return { x: 0, y: 0, width: 1, height: 1 };
95
+ }
96
+ const row = value;
97
+ let x = clampCropSide(row.x, 0);
98
+ let y = clampCropSide(row.y, 0);
99
+ let width = clampCropSide(row.width, 1);
100
+ let height = clampCropSide(row.height, 1);
101
+ width = Math.max(0.05, Math.min(width, 1 - x));
102
+ height = Math.max(0.05, Math.min(height, 1 - y));
103
+ return { x, y, width, height };
104
+ }
105
+ function imageProps(value) {
106
+ let src = "";
107
+ let crop = { x: 0, y: 0, width: 1, height: 1 };
108
+ if (typeof value === "string") {
109
+ src = value;
110
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
111
+ const row = value;
112
+ if (typeof row.url !== "string") return null;
113
+ src = row.url;
114
+ if (row.crop != null) crop = readCrop(row.crop);
115
+ } else {
116
+ return null;
117
+ }
118
+ if (!src) return null;
119
+ return {
120
+ src,
121
+ crop,
122
+ style: {
123
+ position: "absolute",
124
+ left: `${-crop.x / crop.width * 100}%`,
125
+ top: `${-crop.y / crop.height * 100}%`,
126
+ width: `${1 / crop.width * 100}%`,
127
+ height: `${1 / crop.height * 100}%`,
128
+ maxWidth: "none",
129
+ maxHeight: "none"
130
+ }
131
+ };
132
+ }
88
133
  // Annotate the CommonJS export names for ESM import in node:
89
134
  0 && (module.exports = {
90
135
  getConfig,
91
136
  getEntries,
92
137
  getEntry,
93
138
  getEntryByPreviewToken,
139
+ imageProps,
94
140
  initDispatch
95
141
  });
96
142
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Entry, DispatchConfig } from \"./client\";\nexport {\n initDispatch,\n getConfig,\n getEntries,\n getEntry,\n getEntryByPreviewToken,\n} from \"./client\";\n","export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAE7B,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,WAAW,YAAoB,SAAoC;AACvF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC;AAC9E,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,SAAS,YAAoB,MAAc,SAAyC;AACxG,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAAC;AAC1G,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAGA,eAAsB,uBAAuB,OAAsC;AACjF,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,QAAM,MAAM,GAAG,iBAAiB,sBAAsB,mBAAmB,MAAM,KAAK,CAAC,CAAC;AACtF,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts"],"sourcesContent":["export type { Entry, DispatchConfig, ImageCrop } from \"./client\";\nexport {\n initDispatch,\n getConfig,\n getEntries,\n getEntry,\n getEntryByPreviewToken,\n imageProps,\n} from \"./client\";\n","export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\nexport type ImageCrop = { x: number; y: number; width: number; height: number };\n\nfunction clampCropSide(n: number, fallback: number) {\n return typeof n === \"number\" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;\n}\n\nfunction readCrop(value: unknown): ImageCrop {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { x: 0, y: 0, width: 1, height: 1 };\n }\n const row = value as Record<string, unknown>;\n let x = clampCropSide(row.x as number, 0);\n let y = clampCropSide(row.y as number, 0);\n let width = clampCropSide(row.width as number, 1);\n let height = clampCropSide(row.height as number, 1);\n width = Math.max(0.05, Math.min(width, 1 - x));\n height = Math.max(0.05, Math.min(height, 1 - y));\n return { x, y, width, height };\n}\n\n/** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */\nexport function imageProps(value: unknown): {\n src: string;\n crop: ImageCrop;\n style: {\n position: \"absolute\";\n left: string;\n top: string;\n width: string;\n height: string;\n maxWidth: \"none\";\n maxHeight: \"none\";\n };\n} | null {\n let src = \"\";\n let crop: ImageCrop = { x: 0, y: 0, width: 1, height: 1 };\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.crop != null) crop = readCrop(row.crop);\n } else {\n return null;\n }\n if (!src) return null;\n return {\n src,\n crop,\n style: {\n position: \"absolute\",\n left: `${(-crop.x / crop.width) * 100}%`,\n top: `${(-crop.y / crop.height) * 100}%`,\n width: `${(1 / crop.width) * 100}%`,\n height: `${(1 / crop.height) * 100}%`,\n maxWidth: \"none\",\n maxHeight: \"none\",\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -55,11 +55,56 @@ async function getEntryByPreviewToken(token) {
55
55
  }
56
56
  return await res.json();
57
57
  }
58
+ function clampCropSide(n, fallback) {
59
+ return typeof n === "number" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;
60
+ }
61
+ function readCrop(value) {
62
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
63
+ return { x: 0, y: 0, width: 1, height: 1 };
64
+ }
65
+ const row = value;
66
+ let x = clampCropSide(row.x, 0);
67
+ let y = clampCropSide(row.y, 0);
68
+ let width = clampCropSide(row.width, 1);
69
+ let height = clampCropSide(row.height, 1);
70
+ width = Math.max(0.05, Math.min(width, 1 - x));
71
+ height = Math.max(0.05, Math.min(height, 1 - y));
72
+ return { x, y, width, height };
73
+ }
74
+ function imageProps(value) {
75
+ let src = "";
76
+ let crop = { x: 0, y: 0, width: 1, height: 1 };
77
+ if (typeof value === "string") {
78
+ src = value;
79
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
80
+ const row = value;
81
+ if (typeof row.url !== "string") return null;
82
+ src = row.url;
83
+ if (row.crop != null) crop = readCrop(row.crop);
84
+ } else {
85
+ return null;
86
+ }
87
+ if (!src) return null;
88
+ return {
89
+ src,
90
+ crop,
91
+ style: {
92
+ position: "absolute",
93
+ left: `${-crop.x / crop.width * 100}%`,
94
+ top: `${-crop.y / crop.height * 100}%`,
95
+ width: `${1 / crop.width * 100}%`,
96
+ height: `${1 / crop.height * 100}%`,
97
+ maxWidth: "none",
98
+ maxHeight: "none"
99
+ }
100
+ };
101
+ }
58
102
  export {
59
103
  getConfig,
60
104
  getEntries,
61
105
  getEntry,
62
106
  getEntryByPreviewToken,
107
+ imageProps,
63
108
  initDispatch
64
109
  };
65
110
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n"],"mappings":";AAcA,IAAM,oBAAoB;AAM1B,IAAI,SAAgC;AAE7B,SAAS,aAAa,SAA+B;AAC1D,WAAS,EAAE,SAAS,QAAQ,QAAQ;AACtC;AAEO,SAAS,YAAiC;AAC/C,QAAM,SAAS,OAAO,YAAY,cAAc,QAAQ,KAAK,gCAAgC;AAC7F,QAAM,UAAU,QAAQ,YAAY,OAAO,WAAW,WAAW,SAAS;AAC1E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,UAAM,IAAI,MAAM,qGAAqG;AAAA,EACvH;AACA,SAAO,EAAE,QAAQ;AACnB;AAEA,eAAsB,WAAW,YAAoB,SAAoC;AACvF,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC;AAC9E,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAEA,eAAsB,SAAS,YAAoB,MAAc,SAAyC;AACxG,QAAM,EAAE,SAAS,IAAI,IAAI,UAAU;AACnC,QAAM,cAAc,WAAW;AAC/B,QAAM,MAAM,GAAG,iBAAiB,gBAAgB,mBAAmB,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAAC;AAC1G,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,SAAS,EAAE,cAAc,YAAY;AAAA,EACvC,CAAC;AACD,MAAI,IAAI,WAAW,KAAK;AACtB,WAAO;AAAA,EACT;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,KAAK;AACtB,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC1E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAGA,eAAsB,uBAAuB,OAAsC;AACjF,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,QAAM,MAAM,GAAG,iBAAiB,sBAAsB,mBAAmB,MAAM,KAAK,CAAC,CAAC;AACtF,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EAC5E;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["export type Entry = {\n id: string;\n created_at: string;\n published_at: string | null;\n updated_at: string;\n site_id: string;\n collection: string;\n title: string;\n slug: string | null;\n data: Record<string, unknown>;\n sort_order: number;\n published: boolean;\n};\n\nconst DISPATCH_API_BASE = \"https://dispatch-cms.vercel.app\";\n\nexport type DispatchConfig = {\n siteKey: string;\n};\n\nlet config: DispatchConfig | null = null;\n\nexport function initDispatch(options: DispatchConfig): void {\n config = { siteKey: options.siteKey };\n}\n\nexport function getConfig(): { siteKey: string } {\n const envKey = typeof process !== \"undefined\" ? process.env?.NEXT_PUBLIC_DISPATCH_SITE_KEY : undefined;\n const siteKey = config?.siteKey ?? (typeof envKey === \"string\" ? envKey : undefined);\n if (!siteKey || typeof siteKey !== \"string\") {\n throw new Error(\"Dispatch site key is required. Call initDispatch({ siteKey }) or set NEXT_PUBLIC_DISPATCH_SITE_KEY.\");\n }\n return { siteKey };\n}\n\nexport async function getEntries(collection: string, siteKey?: string): Promise<Entry[]> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entries: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry[];\n}\n\nexport async function getEntry(collection: string, slug: string, siteKey?: string): Promise<Entry | null> {\n const { siteKey: key } = getConfig();\n const resolvedKey = siteKey ?? key;\n const url = `${DISPATCH_API_BASE}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}`;\n const res = await fetch(url, {\n headers: { \"X-Site-Key\": resolvedKey },\n });\n if (res.status === 404) {\n return null;\n }\n if (!res.ok) {\n if (res.status === 401) {\n throw new Error(\"Invalid or missing site key\");\n }\n throw new Error(`Failed to fetch entry: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\n/** Draft or published entry. The token is the secret; no site key is required. */\nexport async function getEntryByPreviewToken(token: string): Promise<Entry | null> {\n if (!token?.trim()) return null;\n const url = `${DISPATCH_API_BASE}/api/preview?token=${encodeURIComponent(token.trim())}`;\n const res = await fetch(url);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Failed to fetch preview: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Entry;\n}\n\nexport type ImageCrop = { x: number; y: number; width: number; height: number };\n\nfunction clampCropSide(n: number, fallback: number) {\n return typeof n === \"number\" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;\n}\n\nfunction readCrop(value: unknown): ImageCrop {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { x: 0, y: 0, width: 1, height: 1 };\n }\n const row = value as Record<string, unknown>;\n let x = clampCropSide(row.x as number, 0);\n let y = clampCropSide(row.y as number, 0);\n let width = clampCropSide(row.width as number, 1);\n let height = clampCropSide(row.height as number, 1);\n width = Math.max(0.05, Math.min(width, 1 - x));\n height = Math.max(0.05, Math.min(height, 1 - y));\n return { x, y, width, height };\n}\n\n/** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */\nexport function imageProps(value: unknown): {\n src: string;\n crop: ImageCrop;\n style: {\n position: \"absolute\";\n left: string;\n top: string;\n width: string;\n height: string;\n maxWidth: \"none\";\n maxHeight: \"none\";\n };\n} | null {\n let src = \"\";\n let crop: ImageCrop = { x: 0, y: 0, width: 1, height: 1 };\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.crop != null) crop = readCrop(row.crop);\n } else {\n return null;\n }\n if (!src) return null;\n return {\n src,\n crop,\n style: {\n position: \"absolute\",\n left: `${(-crop.x / crop.width) * 100}%`,\n top: `${(-crop.y / crop.height) * 100}%`,\n width: `${(1 / crop.width) * 100}%`,\n height: `${(1 / crop.height) * 100}%`,\n maxWidth: \"none\",\n maxHeight: \"none\",\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dispatchcms/next",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Fetch published entries from a Dispatch CMS in Next.js",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -12,6 +12,11 @@
12
12
  "require": "./dist/index.js"
13
13
  }
14
14
  },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "test": "tsx test/client.cache.test.ts && tsx test/image-props.test.ts",
18
+ "test:manual": "tsx test/getPage.test.ts"
19
+ },
15
20
  "devDependencies": {
16
21
  "@types/node": "^22.20.1",
17
22
  "next": "^16.3.0",
@@ -31,10 +36,5 @@
31
36
  "license": "MIT",
32
37
  "peerDependencies": {
33
38
  "next": ">=16.0.0"
34
- },
35
- "scripts": {
36
- "build": "tsup",
37
- "test": "tsx test/client.cache.test.ts",
38
- "test:manual": "tsx test/getPage.test.ts"
39
39
  }
40
- }
40
+ }
package/src/client.ts CHANGED
@@ -79,3 +79,65 @@ export async function getEntryByPreviewToken(token: string): Promise<Entry | nul
79
79
  }
80
80
  return (await res.json()) as Entry;
81
81
  }
82
+
83
+ export type ImageCrop = { x: number; y: number; width: number; height: number };
84
+
85
+ function clampCropSide(n: number, fallback: number) {
86
+ return typeof n === "number" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallback;
87
+ }
88
+
89
+ function readCrop(value: unknown): ImageCrop {
90
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
91
+ return { x: 0, y: 0, width: 1, height: 1 };
92
+ }
93
+ const row = value as Record<string, unknown>;
94
+ let x = clampCropSide(row.x as number, 0);
95
+ let y = clampCropSide(row.y as number, 0);
96
+ let width = clampCropSide(row.width as number, 1);
97
+ let height = clampCropSide(row.height as number, 1);
98
+ width = Math.max(0.05, Math.min(width, 1 - x));
99
+ height = Math.max(0.05, Math.min(height, 1 - y));
100
+ return { x, y, width, height };
101
+ }
102
+
103
+ /** Styles for an <img> inside a sized overflow:hidden frame — shows only the cropped region. */
104
+ export function imageProps(value: unknown): {
105
+ src: string;
106
+ crop: ImageCrop;
107
+ style: {
108
+ position: "absolute";
109
+ left: string;
110
+ top: string;
111
+ width: string;
112
+ height: string;
113
+ maxWidth: "none";
114
+ maxHeight: "none";
115
+ };
116
+ } | null {
117
+ let src = "";
118
+ let crop: ImageCrop = { x: 0, y: 0, width: 1, height: 1 };
119
+ if (typeof value === "string") {
120
+ src = value;
121
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
122
+ const row = value as Record<string, unknown>;
123
+ if (typeof row.url !== "string") return null;
124
+ src = row.url;
125
+ if (row.crop != null) crop = readCrop(row.crop);
126
+ } else {
127
+ return null;
128
+ }
129
+ if (!src) return null;
130
+ return {
131
+ src,
132
+ crop,
133
+ style: {
134
+ position: "absolute",
135
+ left: `${(-crop.x / crop.width) * 100}%`,
136
+ top: `${(-crop.y / crop.height) * 100}%`,
137
+ width: `${(1 / crop.width) * 100}%`,
138
+ height: `${(1 / crop.height) * 100}%`,
139
+ maxWidth: "none",
140
+ maxHeight: "none",
141
+ },
142
+ };
143
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,9 @@
1
- export type { Entry, DispatchConfig } from "./client";
1
+ export type { Entry, DispatchConfig, ImageCrop } from "./client";
2
2
  export {
3
3
  initDispatch,
4
4
  getConfig,
5
5
  getEntries,
6
6
  getEntry,
7
7
  getEntryByPreviewToken,
8
+ imageProps,
8
9
  } from "./client";
@@ -0,0 +1,22 @@
1
+ import { imageProps } from "../src/index";
2
+ import assert from "node:assert/strict";
3
+
4
+ const url = "https://cdn.example/a.jpg";
5
+
6
+ const plain = imageProps(url);
7
+ assert(plain?.src === url && plain.crop.width === 1 && plain.crop.height === 1, "plain url");
8
+ assert(plain?.style.left === "0%" && plain.style.top === "0%", "full crop origin");
9
+ assert(plain?.style.width === "100%" && plain.style.height === "100%", "full crop size");
10
+
11
+ const cropped = imageProps({ url, crop: { x: 0.2, y: 0.1, width: 0.5, height: 0.4 } });
12
+ assert(cropped?.src === url, "crop src");
13
+ assert(cropped?.crop.x === 0.2 && cropped.crop.width === 0.5, "crop rect");
14
+ assert(cropped?.style.left === "-40%" && cropped.style.top === "-25%", "crop offset");
15
+ assert(cropped?.style.width === "200%" && cropped.style.height === "250%", "crop scale");
16
+
17
+ const legacyFocus = imageProps({ url, focus: { x: 0.2, y: 0.8 } });
18
+ assert(legacyFocus?.src === url && legacyFocus.crop.width === 1, "legacy focus becomes full frame");
19
+
20
+ assert(imageProps(null) === null && imageProps("") === null && imageProps({}) === null, "bad input");
21
+
22
+ console.log("image-props: ok");