astro-tractstack 2.0.0-rc.54 → 2.0.0-rc.55
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/dist/index.js +12 -8
- package/package.json +1 -1
- package/templates/custom/minimal/CodeHook.astro +8 -6
- package/templates/custom/with-examples/CodeHook.astro +8 -4
- package/templates/src/components/codehooks/FeaturedArticle.astro +109 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +283 -0
- package/templates/src/components/codehooks/ListContent.astro +21 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +1 -4
- package/templates/src/components/codehooks/SearchWidget.tsx +444 -0
- package/templates/src/components/compositor/nodes/Pane.tsx +12 -14
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +6 -1
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +4 -44
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/search/SearchModal.tsx +19 -3
- package/templates/src/types/tractstack.ts +1 -1
- package/templates/src/utils/helpers.ts +2 -2
- package/utils/inject-files.ts +10 -6
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
package/dist/index.js
CHANGED
|
@@ -549,9 +549,9 @@ async function w(t, e, c) {
|
|
|
549
549
|
},
|
|
550
550
|
{
|
|
551
551
|
src: t(
|
|
552
|
-
"../templates/src/components/compositor/preview/
|
|
552
|
+
"../templates/src/components/compositor/preview/FeaturedArticlePreview.tsx"
|
|
553
553
|
),
|
|
554
|
-
dest: "src/components/compositor/preview/
|
|
554
|
+
dest: "src/components/compositor/preview/FeaturedArticlePreview.tsx"
|
|
555
555
|
},
|
|
556
556
|
// Server side stores
|
|
557
557
|
{
|
|
@@ -1216,17 +1216,21 @@ async function w(t, e, c) {
|
|
|
1216
1216
|
src: t("../templates/src/components/codehooks/SankeyDiagram.tsx"),
|
|
1217
1217
|
dest: "src/components/codehooks/SankeyDiagram.tsx"
|
|
1218
1218
|
},
|
|
1219
|
+
{
|
|
1220
|
+
src: t("../templates/src/components/codehooks/SearchWidget.tsx"),
|
|
1221
|
+
dest: "src/components/codehooks/SearchWidget.tsx"
|
|
1222
|
+
},
|
|
1219
1223
|
{
|
|
1220
1224
|
src: t(
|
|
1221
|
-
"../templates/src/components/codehooks/
|
|
1225
|
+
"../templates/src/components/codehooks/FeaturedArticle.astro"
|
|
1222
1226
|
),
|
|
1223
|
-
dest: "src/components/codehooks/
|
|
1227
|
+
dest: "src/components/codehooks/FeaturedArticle.astro"
|
|
1224
1228
|
},
|
|
1225
1229
|
{
|
|
1226
1230
|
src: t(
|
|
1227
|
-
"../templates/src/components/codehooks/
|
|
1231
|
+
"../templates/src/components/codehooks/FeaturedArticleSetup.tsx"
|
|
1228
1232
|
),
|
|
1229
|
-
dest: "src/components/codehooks/
|
|
1233
|
+
dest: "src/components/codehooks/FeaturedArticleSetup.tsx"
|
|
1230
1234
|
},
|
|
1231
1235
|
{
|
|
1232
1236
|
src: t("../templates/src/components/codehooks/ListContent.astro"),
|
|
@@ -2087,7 +2091,7 @@ export default function Placeholder() {
|
|
|
2087
2091
|
}` : t.endsWith(".ts") ? `// TractStack placeholder utility
|
|
2088
2092
|
export const placeholder = "${t}";` : `# TractStack placeholder: ${t}`;
|
|
2089
2093
|
}
|
|
2090
|
-
function
|
|
2094
|
+
function C(t = {}) {
|
|
2091
2095
|
const { resolve: e } = b(import.meta.url);
|
|
2092
2096
|
return {
|
|
2093
2097
|
name: "astro-tractstack",
|
|
@@ -2169,5 +2173,5 @@ function S(t = {}) {
|
|
|
2169
2173
|
};
|
|
2170
2174
|
}
|
|
2171
2175
|
export {
|
|
2172
|
-
|
|
2176
|
+
C as default
|
|
2173
2177
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import FeaturedArticle from '@/components/codehooks/FeaturedArticle.astro';
|
|
3
3
|
import ListContent from '@/components/codehooks/ListContent.astro';
|
|
4
|
+
import SearchWidget from '@/components/codehooks/SearchWidget.tsx';
|
|
4
5
|
import BunnyVideoWrapper from '@/components/codehooks/BunnyVideoWrapper.astro';
|
|
5
6
|
import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
|
|
6
7
|
import type { FullContentMapItem } from '@/types/tractstack';
|
|
@@ -21,8 +22,10 @@ export interface Props {
|
|
|
21
22
|
const { target, options, fullContentMap /*, resourcesPayload */ } = Astro.props;
|
|
22
23
|
|
|
23
24
|
export const components = {
|
|
25
|
+
'featured-article': true,
|
|
24
26
|
'featured-content': true,
|
|
25
27
|
'list-content': true,
|
|
28
|
+
'search-widget': true,
|
|
26
29
|
'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
|
|
27
30
|
epinet: true,
|
|
28
31
|
// "custom-hero": true, // Uncomment when you create CustomHero.astro
|
|
@@ -32,8 +35,10 @@ export const components = {
|
|
|
32
35
|
{
|
|
33
36
|
target === 'list-content' ? (
|
|
34
37
|
<ListContent options={options} contentMap={fullContentMap} />
|
|
35
|
-
) : target === 'featured-
|
|
36
|
-
<
|
|
38
|
+
) : target === 'featured-article' ? (
|
|
39
|
+
<FeaturedArticle options={options} contentMap={fullContentMap} />
|
|
40
|
+
) : target === 'search-widget' ? (
|
|
41
|
+
<SearchWidget fullContentMap={fullContentMap} client:load />
|
|
37
42
|
) : target === 'bunny-video' && import.meta.env.PUBLIC_ENABLE_BUNNY ? (
|
|
38
43
|
<BunnyVideoWrapper options={options} />
|
|
39
44
|
) : target === 'epinet' ? (
|
|
@@ -44,9 +49,6 @@ export const components = {
|
|
|
44
49
|
) : (
|
|
45
50
|
<div class="rounded-lg bg-gray-50 p-8 text-center">
|
|
46
51
|
<p class="text-gray-600">CodeHook target "{target}" not found</p>
|
|
47
|
-
<p class="mt-2 text-sm text-gray-500">
|
|
48
|
-
Available: list-content, featured-content, bunny-video, epinet
|
|
49
|
-
</p>
|
|
50
52
|
</div>
|
|
51
53
|
)
|
|
52
54
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import CustomHero from './CustomHero.astro';
|
|
3
|
-
import
|
|
3
|
+
import SearchWidget from '@/components/codehooks/SearchWidget.tsx';
|
|
4
|
+
import FeaturedArticle from '@/components/codehooks/FeaturedArticle.astro';
|
|
4
5
|
import ListContent from '@/components/codehooks/ListContent.astro';
|
|
5
6
|
import BunnyVideoWrapper from '@/components/codehooks/BunnyVideoWrapper.astro';
|
|
6
7
|
import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
|
|
@@ -22,8 +23,9 @@ const { target, options, fullContentMap /*, resourcesPayload */ } = Astro.props;
|
|
|
22
23
|
|
|
23
24
|
export const components = {
|
|
24
25
|
'custom-hero': true,
|
|
25
|
-
'featured-
|
|
26
|
+
'featured-article': true,
|
|
26
27
|
'list-content': true,
|
|
28
|
+
'search-widget': true,
|
|
27
29
|
'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
|
|
28
30
|
epinet: true,
|
|
29
31
|
};
|
|
@@ -32,8 +34,10 @@ export const components = {
|
|
|
32
34
|
{
|
|
33
35
|
target === 'list-content' ? (
|
|
34
36
|
<ListContent options={options} contentMap={fullContentMap} />
|
|
35
|
-
) : target === 'featured-
|
|
36
|
-
<
|
|
37
|
+
) : target === 'featured-article' ? (
|
|
38
|
+
<FeaturedArticle options={options} contentMap={fullContentMap} />
|
|
39
|
+
) : target === 'search-widget' ? (
|
|
40
|
+
<SearchWidget fullContentMap={fullContentMap} client:load />
|
|
37
41
|
) : target === 'bunny-video' && import.meta.env.PUBLIC_ENABLE_BUNNY ? (
|
|
38
42
|
<BunnyVideoWrapper options={options} />
|
|
39
43
|
) : target === 'custom-hero' ? (
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { FullContentMapItem } from '@/types/tractstack';
|
|
3
|
+
|
|
4
|
+
export interface Props {
|
|
5
|
+
options?: {
|
|
6
|
+
params?: {
|
|
7
|
+
options?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
contentMap: FullContentMapItem[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { options, contentMap } = Astro.props;
|
|
14
|
+
|
|
15
|
+
// Parse component options
|
|
16
|
+
let parsedOptions;
|
|
17
|
+
try {
|
|
18
|
+
parsedOptions = JSON.parse(options?.params?.options || '{}');
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.error('Invalid options for FeaturedArticle', e);
|
|
21
|
+
parsedOptions = { slug: '' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const slug = parsedOptions.slug || '';
|
|
25
|
+
|
|
26
|
+
// Find the featured story from the contentMap
|
|
27
|
+
// It must have a description, panes, AND a thumbnail image to be valid
|
|
28
|
+
const featuredStory = contentMap.find(
|
|
29
|
+
(item: FullContentMapItem) =>
|
|
30
|
+
item.slug === slug &&
|
|
31
|
+
item.type === 'StoryFragment' &&
|
|
32
|
+
item.description &&
|
|
33
|
+
item.panes &&
|
|
34
|
+
item.panes.length > 0 &&
|
|
35
|
+
item.thumbSrc
|
|
36
|
+
);
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
featuredStory ? (
|
|
41
|
+
<div class="mx-auto w-full max-w-7xl px-8 py-24">
|
|
42
|
+
{/* A robust grid layout that ensures spacing and alignment */}
|
|
43
|
+
<div class="grid grid-cols-1 items-center gap-x-16 gap-y-12 md:grid-cols-2">
|
|
44
|
+
{/* --- TEXT COLUMN --- */}
|
|
45
|
+
<div class="w-full">
|
|
46
|
+
<a href={`/${featuredStory.slug}`} class="block">
|
|
47
|
+
{/* Constrained width for readability */}
|
|
48
|
+
<div class="max-w-lg pr-12">
|
|
49
|
+
<p class="font-action mb-4 text-lg font-bold uppercase text-gray-500">
|
|
50
|
+
Featured Article
|
|
51
|
+
</p>
|
|
52
|
+
<div class="space-y-6">
|
|
53
|
+
<h2 class="py-2 text-3xl font-bold leading-snug text-black transition-colors md:text-4xl xl:text-5xl">
|
|
54
|
+
{featuredStory.title}
|
|
55
|
+
</h2>
|
|
56
|
+
{featuredStory.description && (
|
|
57
|
+
<p class="text-sm leading-relaxed text-gray-700 md:text-lg xl:text-xl">
|
|
58
|
+
{featuredStory.description}
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
{featuredStory.topics && featuredStory.topics.length > 0 && (
|
|
62
|
+
<div class="flex flex-wrap gap-2 pb-6 pt-2">
|
|
63
|
+
{featuredStory.topics.map((topic: string) => (
|
|
64
|
+
<span class="inline-flex items-center rounded-full bg-cyan-100 px-3 py-1 text-sm font-bold text-cyan-800">
|
|
65
|
+
{topic}
|
|
66
|
+
</span>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</a>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* --- IMAGE COLUMN --- */}
|
|
76
|
+
<div class="w-full py-6">
|
|
77
|
+
{/* Max width constraint on the image container */}
|
|
78
|
+
<div class="mx-auto max-w-2xl">
|
|
79
|
+
<a href={`/${featuredStory.slug}`}>
|
|
80
|
+
<img
|
|
81
|
+
src={featuredStory.thumbSrc}
|
|
82
|
+
srcset={featuredStory.thumbSrcSet}
|
|
83
|
+
sizes="(min-width: 768px) 50vw, 100vw"
|
|
84
|
+
alt={`Preview of ${featuredStory.title}`}
|
|
85
|
+
class="w-full rounded-lg shadow-lg"
|
|
86
|
+
style="aspect-ratio: 1200 / 630;"
|
|
87
|
+
/>
|
|
88
|
+
</a>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="md:py-6">
|
|
92
|
+
<a
|
|
93
|
+
href={`/${featuredStory.slug}`}
|
|
94
|
+
class="inline-block w-fit rounded-md bg-gray-100 px-5 py-3 text-sm font-bold text-gray-900 transition-colors hover:bg-gray-200"
|
|
95
|
+
>
|
|
96
|
+
Read More
|
|
97
|
+
</a>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
) : (
|
|
102
|
+
<div class="mx-auto max-w-7xl px-4 py-16">
|
|
103
|
+
<p class="italic text-cyan-600">
|
|
104
|
+
Featured article not found or is missing required content (description,
|
|
105
|
+
panes, thumbnail).
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { Combobox } from '@ark-ui/react';
|
|
4
|
+
import { createListCollection } from '@ark-ui/react/collection';
|
|
5
|
+
import { ChevronUpDownIcon, CheckIcon } from '@heroicons/react/20/solid';
|
|
6
|
+
import { fullContentMapStore, viewportKeyStore } from '@/stores/storykeep';
|
|
7
|
+
import { getCtx } from '@/stores/nodes';
|
|
8
|
+
import { cloneDeep } from '@/utils/helpers';
|
|
9
|
+
import type { PaneNode } from '@/types/compositorTypes';
|
|
10
|
+
|
|
11
|
+
interface FeaturedArticleSetupProps {
|
|
12
|
+
params?: Record<string, string>;
|
|
13
|
+
nodeId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const comboboxItemStyles = `
|
|
17
|
+
.combo-item .check-indicator {
|
|
18
|
+
display: none;
|
|
19
|
+
}
|
|
20
|
+
.combo-item[data-state="checked"] .check-indicator {
|
|
21
|
+
display: flex;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const FeaturedArticleSetup = ({
|
|
26
|
+
params,
|
|
27
|
+
nodeId,
|
|
28
|
+
}: FeaturedArticleSetupProps) => {
|
|
29
|
+
const $contentMap = useStore(fullContentMapStore);
|
|
30
|
+
const $viewportKey = useStore(viewportKeyStore);
|
|
31
|
+
const isInitialMount = useRef(true);
|
|
32
|
+
const ctx = getCtx();
|
|
33
|
+
|
|
34
|
+
const availableStories = useMemo(
|
|
35
|
+
() =>
|
|
36
|
+
$contentMap.filter(
|
|
37
|
+
(item) =>
|
|
38
|
+
item.type === 'StoryFragment' &&
|
|
39
|
+
item.description &&
|
|
40
|
+
item.panes &&
|
|
41
|
+
item.panes.length > 0 &&
|
|
42
|
+
item.thumbSrc
|
|
43
|
+
),
|
|
44
|
+
[$contentMap]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const initialSlug = params?.slug || '';
|
|
48
|
+
const initialStory = availableStories.find(
|
|
49
|
+
(story) => story.slug === initialSlug
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
|
53
|
+
const [selectedSlug, setSelectedSlug] = useState(initialSlug);
|
|
54
|
+
const [query, setQuery] = useState(initialStory?.title || '');
|
|
55
|
+
|
|
56
|
+
const selectedStory = useMemo(
|
|
57
|
+
() => availableStories.find((story) => story.slug === selectedSlug),
|
|
58
|
+
[availableStories, selectedSlug]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const collection = useMemo(() => {
|
|
62
|
+
const filtered =
|
|
63
|
+
query === '' || query === selectedStory?.title
|
|
64
|
+
? availableStories
|
|
65
|
+
: availableStories.filter((story) =>
|
|
66
|
+
story.title.toLowerCase().includes(query.toLowerCase())
|
|
67
|
+
);
|
|
68
|
+
return createListCollection({
|
|
69
|
+
items: filtered,
|
|
70
|
+
itemToValue: (item) => item.slug,
|
|
71
|
+
itemToString: (item) => item.title,
|
|
72
|
+
});
|
|
73
|
+
}, [availableStories, query, selectedStory]);
|
|
74
|
+
|
|
75
|
+
const updatePaneNode = () => {
|
|
76
|
+
if (!nodeId) return;
|
|
77
|
+
const allNodes = ctx.allNodes.get();
|
|
78
|
+
const paneNode = cloneDeep(allNodes.get(nodeId)) as PaneNode;
|
|
79
|
+
if (paneNode) {
|
|
80
|
+
const updatedNode = {
|
|
81
|
+
...paneNode,
|
|
82
|
+
codeHookTarget: 'featured-article',
|
|
83
|
+
codeHookPayload: {
|
|
84
|
+
options: JSON.stringify({ slug: selectedSlug }),
|
|
85
|
+
},
|
|
86
|
+
isChanged: true,
|
|
87
|
+
};
|
|
88
|
+
ctx.modifyNodes([updatedNode]);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (isInitialMount.current) {
|
|
94
|
+
isInitialMount.current = false;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const timeoutId = setTimeout(updatePaneNode, 500);
|
|
98
|
+
return () => clearTimeout(timeoutId);
|
|
99
|
+
}, [selectedSlug]);
|
|
100
|
+
|
|
101
|
+
const handleSelection = (details: { value: string[] }) => {
|
|
102
|
+
const slug = details.value[0] || '';
|
|
103
|
+
setSelectedSlug(slug);
|
|
104
|
+
const story = availableStories.find((s) => s.slug === slug);
|
|
105
|
+
if (story) {
|
|
106
|
+
setQuery(story.title);
|
|
107
|
+
} else {
|
|
108
|
+
setQuery('');
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const renderPreview = () => {
|
|
113
|
+
if (!selectedStory) return null;
|
|
114
|
+
|
|
115
|
+
const topics = selectedStory.topics && selectedStory.topics.length > 0 && (
|
|
116
|
+
<div className="flex flex-wrap gap-2 pt-2">
|
|
117
|
+
{selectedStory.topics.map((topic) => (
|
|
118
|
+
<span
|
|
119
|
+
key={topic}
|
|
120
|
+
className="inline-flex items-center rounded-full bg-cyan-100 px-3 py-1 text-sm font-bold text-cyan-800"
|
|
121
|
+
>
|
|
122
|
+
{topic}
|
|
123
|
+
</span>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Mobile is the default, single-column layout
|
|
129
|
+
if ($viewportKey.value === 'mobile') {
|
|
130
|
+
return (
|
|
131
|
+
<div className="flex flex-col gap-8 pt-4">
|
|
132
|
+
<div className="w-full">
|
|
133
|
+
<p className="font-action text-md mb-4 font-bold uppercase text-gray-500">
|
|
134
|
+
Featured Article
|
|
135
|
+
</p>
|
|
136
|
+
<div className="space-y-6">
|
|
137
|
+
<h2 className="text-4xl font-bold leading-snug">
|
|
138
|
+
{selectedStory.title}
|
|
139
|
+
</h2>
|
|
140
|
+
<p className="text-lg leading-loose text-gray-700">
|
|
141
|
+
{selectedStory.description}
|
|
142
|
+
</p>
|
|
143
|
+
{topics}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="w-full">
|
|
147
|
+
<img
|
|
148
|
+
src={selectedStory.thumbSrc}
|
|
149
|
+
srcSet={selectedStory.thumbSrcSet}
|
|
150
|
+
alt={`Preview of ${selectedStory.title}`}
|
|
151
|
+
className="w-full rounded-lg shadow-lg"
|
|
152
|
+
style={{ aspectRatio: '1200 / 630' }}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Tablet and Desktop share the same two-column layout
|
|
160
|
+
return (
|
|
161
|
+
<div className="flex flex-row items-center gap-12 pt-4">
|
|
162
|
+
<div className="w-3/5">
|
|
163
|
+
<p className="font-action text-md mb-4 font-bold uppercase text-gray-500">
|
|
164
|
+
Featured Article
|
|
165
|
+
</p>
|
|
166
|
+
<div className="space-y-6">
|
|
167
|
+
<h2 className="text-5xl font-bold leading-snug">
|
|
168
|
+
{selectedStory.title}
|
|
169
|
+
</h2>
|
|
170
|
+
<p className="text-lg leading-loose text-gray-700">
|
|
171
|
+
{selectedStory.description}
|
|
172
|
+
</p>
|
|
173
|
+
{topics}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="w-2/5">
|
|
177
|
+
<img
|
|
178
|
+
src={selectedStory.thumbSrc}
|
|
179
|
+
srcSet={selectedStory.thumbSrcSet}
|
|
180
|
+
alt={`Preview of ${selectedStory.title}`}
|
|
181
|
+
className="w-full rounded-lg shadow-lg"
|
|
182
|
+
style={{ aspectRatio: '1200 / 630' }}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (!isPanelOpen) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="flex min-h-[200px] w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => setIsPanelOpen(true)}
|
|
194
|
+
className="rounded-lg bg-cyan-600 px-6 py-3 font-bold text-white shadow-md transition-colors hover:bg-cyan-700"
|
|
195
|
+
>
|
|
196
|
+
{selectedStory
|
|
197
|
+
? 'Edit Featured Article'
|
|
198
|
+
: 'Configure Featured Article'}
|
|
199
|
+
</button>
|
|
200
|
+
{selectedStory && (
|
|
201
|
+
<div className="mt-3 text-center text-sm text-gray-600">
|
|
202
|
+
Currently featuring:
|
|
203
|
+
<br />
|
|
204
|
+
<span className="font-bold">{selectedStory.title}</span>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div className="w-full space-y-6 bg-slate-50 p-6">
|
|
213
|
+
<style>{comboboxItemStyles}</style>
|
|
214
|
+
<div className="flex items-center justify-between">
|
|
215
|
+
<h2 className="text-xl font-bold text-gray-900">
|
|
216
|
+
Configure Featured Article
|
|
217
|
+
</h2>
|
|
218
|
+
<button
|
|
219
|
+
onClick={() => setIsPanelOpen(false)}
|
|
220
|
+
className="rounded bg-gray-200 px-4 py-2 font-bold text-gray-800 transition-colors hover:bg-gray-300"
|
|
221
|
+
>
|
|
222
|
+
Close
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="rounded-lg bg-white p-4 shadow">
|
|
227
|
+
<label className="block text-sm font-bold text-gray-700">
|
|
228
|
+
Select an Article
|
|
229
|
+
</label>
|
|
230
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
231
|
+
Only articles with a description, content, and thumbnail will be
|
|
232
|
+
shown.
|
|
233
|
+
</p>
|
|
234
|
+
<Combobox.Root
|
|
235
|
+
collection={collection}
|
|
236
|
+
value={selectedSlug ? [selectedSlug] : []}
|
|
237
|
+
inputValue={query}
|
|
238
|
+
onValueChange={handleSelection}
|
|
239
|
+
onInputValueChange={(details) => setQuery(details.inputValue)}
|
|
240
|
+
className="mt-2"
|
|
241
|
+
>
|
|
242
|
+
<div className="relative">
|
|
243
|
+
<Combobox.Input
|
|
244
|
+
className="w-full rounded-md border border-gray-300 py-2 pl-3 pr-10 text-sm focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500"
|
|
245
|
+
placeholder="Search for an article..."
|
|
246
|
+
/>
|
|
247
|
+
<Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
248
|
+
<ChevronUpDownIcon
|
|
249
|
+
className="h-5 w-5 text-gray-400"
|
|
250
|
+
aria-hidden="true"
|
|
251
|
+
/>
|
|
252
|
+
</Combobox.Trigger>
|
|
253
|
+
</div>
|
|
254
|
+
<Combobox.Content className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
|
255
|
+
{collection.items.map((item) => (
|
|
256
|
+
<Combobox.Item
|
|
257
|
+
key={item.slug}
|
|
258
|
+
item={item}
|
|
259
|
+
className="combo-item relative cursor-default select-none py-2 pl-10 pr-4 text-gray-900 data-[highlighted]:bg-cyan-600 data-[highlighted]:text-white"
|
|
260
|
+
>
|
|
261
|
+
<span className="block truncate">{item.title}</span>
|
|
262
|
+
<span className="check-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
|
|
263
|
+
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
264
|
+
</span>
|
|
265
|
+
</Combobox.Item>
|
|
266
|
+
))}
|
|
267
|
+
</Combobox.Content>
|
|
268
|
+
</Combobox.Root>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{selectedStory && (
|
|
272
|
+
<div className="rounded-lg bg-white p-4 shadow">
|
|
273
|
+
<h3 className="border-b border-gray-200 pb-2 text-lg font-bold">
|
|
274
|
+
Live Preview
|
|
275
|
+
</h3>
|
|
276
|
+
{renderPreview()}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export default FeaturedArticleSetup;
|