@pantheon-systems/create-p1-starter-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/index.js +5 -0
  2. package/lib/cli.js +149 -0
  3. package/lib/copy-template.js +67 -0
  4. package/lib/install-deps.js +68 -0
  5. package/lib/messages.js +28 -0
  6. package/package.json +38 -0
  7. package/template/.env.example +10 -0
  8. package/template/README.md +53 -0
  9. package/template/__tests__/editor-integration.test.ts +53 -0
  10. package/template/__tests__/remote-datasource-fetchers.test.ts +226 -0
  11. package/template/app/[...puckPath]/client.tsx +9 -0
  12. package/template/app/[...puckPath]/page.tsx +131 -0
  13. package/template/app/collection-nav.tsx +47 -0
  14. package/template/app/layout.tsx +13 -0
  15. package/template/app/p1/[[...p1]]/editor-client.tsx +116 -0
  16. package/template/app/p1/[[...p1]]/page.tsx +19 -0
  17. package/template/app/p1/[[...p1]]/render-client.tsx +9 -0
  18. package/template/app/p1/api/[...p1]/route.ts +16 -0
  19. package/template/app/p1/auth/[...action]/route.ts +9 -0
  20. package/template/app/p1/merge/merge-client.tsx +635 -0
  21. package/template/app/p1/merge/merge.css +257 -0
  22. package/template/app/p1/merge/page.tsx +12 -0
  23. package/template/app/page.tsx +130 -0
  24. package/template/app/styles.css +18 -0
  25. package/template/components/puck/block-padding.ts +2 -0
  26. package/template/components/puck/button-block.tsx +41 -0
  27. package/template/components/puck/divider-block.tsx +10 -0
  28. package/template/components/puck/grid-block.tsx +80 -0
  29. package/template/components/puck/heading-block.tsx +38 -0
  30. package/template/components/puck/image-block.tsx +33 -0
  31. package/template/components/puck/list-block.tsx +72 -0
  32. package/template/components/puck/paragraph-block.tsx +44 -0
  33. package/template/components/puck/quote-block.tsx +23 -0
  34. package/template/components/puck/root.tsx +20 -0
  35. package/template/components/puck/spacer-block.tsx +19 -0
  36. package/template/eslint.config.js +161 -0
  37. package/template/lib/content-publisher.ts +128 -0
  38. package/template/lib/fetcher-helpers.ts +17 -0
  39. package/template/lib/monsters-api.ts +125 -0
  40. package/template/lib/remote-datasource-fetchers.ts +10 -0
  41. package/template/lib/remote-datasources.ts +154 -0
  42. package/template/lib/swapi.ts +75 -0
  43. package/template/next-env.d.ts +6 -0
  44. package/template/next.config.mjs +13 -0
  45. package/template/package.json +42 -0
  46. package/template/postcss.config.mjs +8 -0
  47. package/template/public/sw.js +8 -0
  48. package/template/puck.config.tsx +51 -0
  49. package/template/tsconfig/base.json +20 -0
  50. package/template/tsconfig/nextjs.json +21 -0
  51. package/template/tsconfig.json +16 -0
  52. package/template/vitest.config.ts +7 -0
@@ -0,0 +1,44 @@
1
+ import { blockPaddingClass } from "./block-padding";
2
+ import ReactMarkdown from "react-markdown";
3
+
4
+ function asMarkdownText(value: unknown): string {
5
+ if (typeof value === "string") return value;
6
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
7
+ return "";
8
+ }
9
+
10
+ export const paragraphBlock = {
11
+ label: "Paragraph",
12
+ fields: {
13
+ text: {
14
+ type: "textarea" as const,
15
+ label: "Text",
16
+ contentEditable: true,
17
+ },
18
+ },
19
+ defaultProps: {
20
+ text: "Add your copy here. You can use multiple lines.",
21
+ },
22
+ render: ({ text }: { text?: string }) => {
23
+ const markdown = asMarkdownText(text);
24
+ return (
25
+ <div className={blockPaddingClass}>
26
+ <ReactMarkdown
27
+ components={{
28
+ p: ({ children }) => <p className="m-0 max-w-prose leading-relaxed">{children}</p>,
29
+ a: ({ href, children }) => (
30
+ <a
31
+ href={href}
32
+ className="text-blue-700 underline decoration-blue-700/40 underline-offset-2 hover:decoration-blue-700"
33
+ >
34
+ {children}
35
+ </a>
36
+ ),
37
+ }}
38
+ >
39
+ {markdown}
40
+ </ReactMarkdown>
41
+ </div>
42
+ );
43
+ },
44
+ };
@@ -0,0 +1,23 @@
1
+ import { blockPaddingClass } from "./block-padding";
2
+
3
+ export const quoteBlock = {
4
+ label: "Quote",
5
+ fields: {
6
+ quote: { type: "textarea" as const, label: "Quote" },
7
+ attribution: { type: "text" as const, label: "Attribution" },
8
+ },
9
+ defaultProps: {
10
+ quote: "A short quotation goes here.",
11
+ attribution: "",
12
+ },
13
+ render: ({ quote, attribution }: { quote?: string; attribution?: string }) => (
14
+ <blockquote
15
+ className={`m-0 max-w-prose border-l-4 border-neutral-300 pl-6 ${blockPaddingClass}`}
16
+ >
17
+ <p className="m-0 text-lg italic leading-relaxed">{quote}</p>
18
+ {attribution ? (
19
+ <footer className="mt-3 text-base text-neutral-600">— {attribution}</footer>
20
+ ) : null}
21
+ </blockquote>
22
+ ),
23
+ };
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export const puckRoot = {
4
+ fields: {
5
+ title: { type: "text" as const },
6
+ description: { type: "textarea" as const },
7
+ },
8
+ defaultProps: {
9
+ title: "My Puck Editor",
10
+ },
11
+ render: (props: { children?: ReactNode; title?: string }) => {
12
+ const { children, title } = props;
13
+ return (
14
+ <div className="font-sans antialiased">
15
+ <h1>{title}</h1>
16
+ {children}
17
+ </div>
18
+ );
19
+ },
20
+ };
@@ -0,0 +1,19 @@
1
+ export const spacerBlock = {
2
+ label: "Spacer",
3
+ fields: {
4
+ height: { type: "number" as const, label: "Height (px)", min: 8, max: 240, step: 4 },
5
+ },
6
+ defaultProps: {
7
+ height: 48,
8
+ },
9
+ render: ({ height }: { height?: number }) => {
10
+ const px = Math.min(240, Math.max(8, height ?? 48));
11
+ return (
12
+ <div
13
+ aria-hidden
14
+ className="w-full min-h-2 max-h-60 shrink-0"
15
+ style={{ height: px }}
16
+ />
17
+ );
18
+ },
19
+ };
@@ -0,0 +1,161 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import importPlugin from 'eslint-plugin-import';
4
+ import globals from 'globals';
5
+ import react from 'eslint-plugin-react';
6
+ import reactHooks from 'eslint-plugin-react-hooks';
7
+ import prettierConfig from 'eslint-config-prettier';
8
+
9
+ export default tseslint.config(
10
+ eslint.configs.recommended,
11
+ ...tseslint.configs.recommended,
12
+ ...tseslint.configs.strict,
13
+ ...tseslint.configs.stylistic,
14
+ {
15
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.mjs'],
16
+ plugins: { import: importPlugin },
17
+ languageOptions: {
18
+ ecmaVersion: 'latest',
19
+ sourceType: 'module',
20
+ globals: {
21
+ ...globals.node,
22
+ ...globals.browser,
23
+ ...globals.es2022,
24
+ },
25
+ },
26
+ rules: {
27
+ // TypeScript specific rules
28
+ '@typescript-eslint/no-unused-vars': [
29
+ 'warn',
30
+ {
31
+ args: 'all',
32
+ argsIgnorePattern: '^_',
33
+ caughtErrors: 'all',
34
+ caughtErrorsIgnorePattern: '^_',
35
+ destructuredArrayIgnorePattern: '^_',
36
+ varsIgnorePattern: '^_',
37
+ ignoreRestSiblings: true,
38
+ },
39
+ ],
40
+ '@typescript-eslint/no-explicit-any': 'warn',
41
+ '@typescript-eslint/no-var-requires': 'warn',
42
+ '@typescript-eslint/consistent-type-imports': 'warn',
43
+ '@typescript-eslint/explicit-function-return-type': 'off',
44
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
45
+ // Import rules
46
+ 'import/extensions': 'off',
47
+ 'import/prefer-default-export': 'off',
48
+ 'import/no-unresolved': 'off',
49
+ 'import/order': 'warn',
50
+ 'import/no-extraneous-dependencies': 'warn',
51
+ // General ESLint rules
52
+ 'no-console': 'off',
53
+ 'no-debugger': 'error',
54
+ 'lines-between-class-members': 'off',
55
+ camelcase: 'off',
56
+ 'no-undef': 'off',
57
+ 'no-underscore-dangle': 'off',
58
+ 'no-unused-vars': 'off',
59
+ 'no-use-before-define': 'off',
60
+ 'no-redeclare': 'off',
61
+ 'no-restricted-syntax': 'off',
62
+ 'no-shadow': 'off',
63
+ 'no-else-return': 'off',
64
+ 'operator-linebreak': 'off',
65
+ 'no-plusplus': 'off',
66
+ 'no-restricted-imports': [
67
+ 'error',
68
+ {
69
+ paths: [
70
+ {
71
+ name: '@/entrypoint',
72
+ message:
73
+ 'Do not import from the entrypoint within source files. Import directly from the module instead.',
74
+ },
75
+ ],
76
+ },
77
+ ],
78
+ // Stricter rules (warn to resolve over time)
79
+ '@typescript-eslint/consistent-generic-constructors': 'warn',
80
+ '@typescript-eslint/consistent-indexed-object-style': 'warn',
81
+ '@typescript-eslint/no-shadow': 'warn',
82
+ '@typescript-eslint/no-require-imports': 'warn',
83
+ 'no-async-promise-executor': 'warn',
84
+ '@typescript-eslint/consistent-type-definitions': 'warn',
85
+ 'no-useless-escape': 'warn',
86
+ 'no-useless-catch': 'warn',
87
+ '@typescript-eslint/no-inferrable-types': 'warn',
88
+ '@typescript-eslint/no-non-null-assertion': 'error',
89
+ 'prefer-const': 'warn',
90
+ '@typescript-eslint/ban-ts-comment': 'warn',
91
+ '@typescript-eslint/no-unused-expressions': 'warn',
92
+ '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
93
+ '@typescript-eslint/no-empty-function': 'warn',
94
+ '@typescript-eslint/no-empty-object-type': 'warn',
95
+ '@typescript-eslint/no-wrapper-object-types': 'warn',
96
+ 'no-constant-binary-expression': 'warn',
97
+ '@typescript-eslint/array-type': 'warn',
98
+ '@typescript-eslint/prefer-for-of': 'warn',
99
+ '@typescript-eslint/no-redeclare': 'warn',
100
+ 'no-case-declarations': 'warn',
101
+ 'no-empty': 'warn',
102
+ '@typescript-eslint/no-dynamic-delete': 'warn',
103
+ 'no-constant-condition': 'warn',
104
+ 'no-empty-pattern': 'warn',
105
+ 'no-var': 'warn',
106
+ '@typescript-eslint/no-namespace': 'warn',
107
+ '@typescript-eslint/no-extraneous-class': 'warn',
108
+ '@typescript-eslint/unified-signatures': 'warn',
109
+ '@typescript-eslint/no-invalid-void-type': 'warn',
110
+ '@typescript-eslint/no-this-alias': 'warn',
111
+ },
112
+ },
113
+ {
114
+ files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
115
+ languageOptions: {
116
+ globals: {
117
+ jest: true,
118
+ },
119
+ },
120
+ },
121
+ {
122
+ ignores: [
123
+ 'node_modules/**',
124
+ 'dist/**',
125
+ 'build/**',
126
+ '.next/**',
127
+ 'coverage/**',
128
+ '**/*.d.ts',
129
+ '**/generated/**',
130
+ '**/.puppeteerrc.cjs',
131
+ ],
132
+ },
133
+ {
134
+ files: ['**/*.{ts,tsx,jsx}'],
135
+ plugins: {
136
+ react,
137
+ 'react-hooks': reactHooks,
138
+ },
139
+ languageOptions: {
140
+ parserOptions: {
141
+ ecmaFeatures: {
142
+ jsx: true,
143
+ },
144
+ },
145
+ },
146
+ settings: {
147
+ react: {
148
+ version: 'detect',
149
+ },
150
+ },
151
+ rules: {
152
+ ...react.configs.recommended.rules,
153
+ ...reactHooks.configs.recommended.rules,
154
+ 'react/react-in-jsx-scope': 'off',
155
+ 'react/prop-types': 'off',
156
+ 'react/display-name': 'off',
157
+ 'react-hooks/exhaustive-deps': 'warn',
158
+ },
159
+ },
160
+ prettierConfig,
161
+ );
@@ -0,0 +1,128 @@
1
+ import type {
2
+ RemoteDatasourceFetcher,
3
+ RemoteDatasourceFetcherParams,
4
+ } from "@pantheon-systems/puck-css/server";
5
+ import { PCCConvenienceFunctions } from "@pantheon-systems/cpub-react-sdk/server";
6
+ import { getFirstValue, savedValue } from "./fetcher-helpers";
7
+
8
+ const ARTICLE_ID_REGEX = /^[a-z0-9][a-z0-9:_-]*$/i;
9
+
10
+ function asArticleId(value: string | undefined): string | undefined {
11
+ if (!value) return undefined;
12
+ const trimmed = value.trim();
13
+ if (!trimmed || !ARTICLE_ID_REGEX.test(trimmed)) return undefined;
14
+ return trimmed;
15
+ }
16
+
17
+ function articleField(row: Record<string, unknown>, key: string): unknown {
18
+ if (key in row) return row[key];
19
+ const attrs = row.attributes;
20
+ if (attrs && typeof attrs === "object" && !Array.isArray(attrs)) {
21
+ return (attrs as Record<string, unknown>)[key];
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ type ArticleListItem = {
27
+ id: string;
28
+ title: string;
29
+ slug?: string;
30
+ url?: string;
31
+ };
32
+
33
+ function mapArticleRow(row: Record<string, unknown>): ArticleListItem | null {
34
+ const idValue =
35
+ articleField(row, "id") ??
36
+ articleField(row, "uuid") ??
37
+ articleField(row, "nid") ??
38
+ articleField(row, "drupal_internal__nid");
39
+ const id =
40
+ typeof idValue === "string"
41
+ ? idValue
42
+ : typeof idValue === "number"
43
+ ? String(idValue)
44
+ : "";
45
+ const titleValue = articleField(row, "title");
46
+ const slugValue = articleField(row, "slug") ?? articleField(row, "path");
47
+ const urlValue = articleField(row, "url");
48
+ const title = typeof titleValue === "string" ? titleValue : "";
49
+ const slug = typeof slugValue === "string" ? slugValue : undefined;
50
+ const url = typeof urlValue === "string" ? urlValue : undefined;
51
+ if (!id || !title) return null;
52
+ return { id, title, slug, url };
53
+ }
54
+
55
+ function arrayFromListPayload(json: unknown): unknown[] {
56
+ if (Array.isArray(json)) return json;
57
+ if (!json || typeof json !== "object") return [];
58
+ const record = json as Record<string, unknown>;
59
+ if (Array.isArray(record.items)) return record.items;
60
+ if (Array.isArray(record.results)) return record.results;
61
+ if (Array.isArray(record.data)) return record.data;
62
+ return [];
63
+ }
64
+
65
+ function resolveArticleId(params: RemoteDatasourceFetcherParams): string | undefined {
66
+ const { searchParams, urlParams, savedPreviewParams } = params;
67
+ return (
68
+ asArticleId(getFirstValue(searchParams, "article")) ??
69
+ asArticleId(getFirstValue(searchParams, "articleId")) ??
70
+ asArticleId(getFirstValue(searchParams, "slug")) ??
71
+ asArticleId(getFirstValue(searchParams, "id")) ??
72
+ asArticleId(savedValue(savedPreviewParams, "article")) ??
73
+ asArticleId(savedValue(savedPreviewParams, "articleId")) ??
74
+ asArticleId(savedValue(savedPreviewParams, "slug")) ??
75
+ asArticleId(savedValue(savedPreviewParams, "id")) ??
76
+ asArticleId(urlParams.article) ??
77
+ asArticleId(urlParams.articleId) ??
78
+ asArticleId(urlParams.slug) ??
79
+ asArticleId(urlParams.id)
80
+ );
81
+ }
82
+
83
+ async function fetchContentPublisherArticle(
84
+ id: string | undefined,
85
+ ): Promise<Record<string, unknown>> {
86
+ const validId = asArticleId(id);
87
+ if (!validId) return {};
88
+ try {
89
+ const article = await PCCConvenienceFunctions.getArticleBySlugOrId(validId, {
90
+ contentType: "TEXT_MARKDOWN",
91
+ });
92
+ if (article && typeof article === "object" && !Array.isArray(article)) {
93
+ return article as unknown as Record<string, unknown>;
94
+ }
95
+ return {};
96
+ } catch {
97
+ return {};
98
+ }
99
+ }
100
+
101
+ async function fetchContentPublisherArticleList(): Promise<ArticleListItem[]> {
102
+ try {
103
+ const payload = await PCCConvenienceFunctions.getPaginatedArticles({
104
+ pageSize: 200,
105
+ });
106
+ const rows = arrayFromListPayload(payload?.data ?? []);
107
+ const out: ArticleListItem[] = [];
108
+ for (const row of rows) {
109
+ if (!row || typeof row !== "object" || Array.isArray(row)) continue;
110
+ const item = mapArticleRow(row as Record<string, unknown>);
111
+ if (item) out.push(item);
112
+ }
113
+ return out;
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
118
+
119
+ export const CONTENT_PUBLISHER_FETCHERS: RemoteDatasourceFetcher[] = [
120
+ {
121
+ id: "article",
122
+ fetch: async (params) => fetchContentPublisherArticle(resolveArticleId(params)),
123
+ },
124
+ {
125
+ id: "article_list",
126
+ fetch: async () => ({ items: await fetchContentPublisherArticleList() }),
127
+ },
128
+ ];
@@ -0,0 +1,17 @@
1
+ export function getFirstValue(
2
+ searchParams: Record<string, string | string[] | undefined>,
3
+ key: string,
4
+ ): string | undefined {
5
+ const raw = searchParams[key];
6
+ if (raw === undefined) return undefined;
7
+ if (Array.isArray(raw)) return raw[0];
8
+ return raw;
9
+ }
10
+
11
+ export function savedValue(
12
+ savedPreviewParams: Record<string, string>,
13
+ key: string,
14
+ ): string | undefined {
15
+ const v = savedPreviewParams[key]?.trim();
16
+ return v || undefined;
17
+ }
@@ -0,0 +1,125 @@
1
+ import type {
2
+ RemoteDatasourceFetcher,
3
+ RemoteDatasourceFetcherParams,
4
+ } from "@pantheon-systems/puck-css/server";
5
+ import { getFirstValue, savedValue } from "./fetcher-helpers";
6
+
7
+ const GRAPHQL_POKEMON_ENDPOINT = "https://graphqlpokemon.favware.tech/v8";
8
+ const MONSTER_INDEX_REGEX = /^[a-z0-9][a-z0-9_-]*$/i;
9
+
10
+ function asMonsterIndex(
11
+ value: string | undefined,
12
+ allowNumeric = true,
13
+ ): string | undefined {
14
+ if (!value) return undefined;
15
+ if (!MONSTER_INDEX_REGEX.test(value)) return undefined;
16
+ if (!allowNumeric && /^\d+$/.test(value)) return undefined;
17
+ return value;
18
+ }
19
+
20
+ function resolveMonsterIndex(params: RemoteDatasourceFetcherParams): string | undefined {
21
+ const { searchParams, urlParams, savedPreviewParams } = params;
22
+ return (
23
+ asMonsterIndex(getFirstValue(searchParams, "monster")) ??
24
+ asMonsterIndex(getFirstValue(searchParams, "monsterIndex")) ??
25
+ asMonsterIndex(getFirstValue(searchParams, "index")) ??
26
+ asMonsterIndex(getFirstValue(searchParams, "id"), false) ??
27
+ asMonsterIndex(savedValue(savedPreviewParams, "monster")) ??
28
+ asMonsterIndex(savedValue(savedPreviewParams, "monsterIndex")) ??
29
+ asMonsterIndex(savedValue(savedPreviewParams, "index")) ??
30
+ asMonsterIndex(savedValue(savedPreviewParams, "id"), false) ??
31
+ asMonsterIndex(urlParams.monster) ??
32
+ asMonsterIndex(urlParams.monsterIndex) ??
33
+ asMonsterIndex(urlParams.index) ??
34
+ asMonsterIndex(urlParams.id, false)
35
+ );
36
+ }
37
+
38
+ async function fetchMonster(
39
+ index: string | undefined,
40
+ fetchImpl: typeof fetch,
41
+ ): Promise<Record<string, unknown>> {
42
+ if (!index || !MONSTER_INDEX_REGEX.test(index)) return {};
43
+ try {
44
+ const res = await fetchImpl(GRAPHQL_POKEMON_ENDPOINT, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({
48
+ query: `query GetPokemon($pokemon: PokemonEnum!) {
49
+ getPokemon(pokemon: $pokemon) {
50
+ key
51
+ num
52
+ species
53
+ types
54
+ abilities { first second hidden special }
55
+ hp attack defense spAtk spDef speed sprite
56
+ }
57
+ }`,
58
+ variables: { pokemon: index },
59
+ }),
60
+ });
61
+ if (!res.ok) return {};
62
+ const json: unknown = await res.json();
63
+ if (!json || typeof json !== "object" || Array.isArray(json)) return {};
64
+ const data = (json as { data?: unknown }).data;
65
+ if (!data || typeof data !== "object" || Array.isArray(data)) return {};
66
+ const pokemon = (data as { getPokemon?: unknown }).getPokemon;
67
+ if (pokemon && typeof pokemon === "object" && !Array.isArray(pokemon)) {
68
+ const row = pokemon as Record<string, unknown>;
69
+ return { ...row, index: row.key, name: row.species, url: `/pokemon/${index}` };
70
+ }
71
+ return {};
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+
77
+ async function fetchMonsterList(
78
+ fetchImpl: typeof fetch,
79
+ ): Promise<Array<{ index: string; name: string; url?: string }>> {
80
+ try {
81
+ const res = await fetchImpl(GRAPHQL_POKEMON_ENDPOINT, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({
85
+ query: `query GetAllPokemon($take: Int!, $offset: Int!) {
86
+ getAllPokemon(take: $take, offset: $offset) {
87
+ key
88
+ species
89
+ }
90
+ }`,
91
+ variables: { take: 200, offset: 89 },
92
+ }),
93
+ });
94
+ if (!res.ok) return [];
95
+ const json: unknown = await res.json();
96
+ if (!json || typeof json !== "object" || Array.isArray(json)) return [];
97
+ const data = (json as { data?: unknown }).data;
98
+ if (!data || typeof data !== "object" || Array.isArray(data)) return [];
99
+ const results = (data as { getAllPokemon?: unknown }).getAllPokemon;
100
+ if (!Array.isArray(results)) return [];
101
+ const out: Array<{ index: string; name: string; url?: string }> = [];
102
+ for (const row of results) {
103
+ if (!row || typeof row !== "object" || Array.isArray(row)) continue;
104
+ const r = row as Record<string, unknown>;
105
+ const index = typeof r.key === "string" ? r.key : "";
106
+ const name = typeof r.species === "string" ? r.species : "";
107
+ const url = index ? `/pokemon/${index}` : undefined;
108
+ if (index && name) out.push({ index, name, url });
109
+ }
110
+ return out;
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
115
+
116
+ export const MONSTER_FETCHERS: RemoteDatasourceFetcher[] = [
117
+ {
118
+ id: "monster",
119
+ fetch: async (params) => fetchMonster(resolveMonsterIndex(params), params.fetchImpl),
120
+ },
121
+ {
122
+ id: "monster_list",
123
+ fetch: async (params) => ({ items: await fetchMonsterList(params.fetchImpl) }),
124
+ },
125
+ ];
@@ -0,0 +1,10 @@
1
+ import type { RemoteDatasourceFetcher } from "@pantheon-systems/puck-css/server";
2
+ import { SWAPI_FETCHERS } from "./swapi";
3
+ import { MONSTER_FETCHERS } from "./monsters-api";
4
+ import { CONTENT_PUBLISHER_FETCHERS } from "./content-publisher";
5
+
6
+ export const REMOTE_DATASOURCE_FETCHERS: RemoteDatasourceFetcher[] = [
7
+ ...SWAPI_FETCHERS,
8
+ ...MONSTER_FETCHERS,
9
+ ...CONTENT_PUBLISHER_FETCHERS,
10
+ ];