astro-tractstack 2.0.0-rc.9 → 2.0.1
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/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +31 -8
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -9
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- 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/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -0,0 +1,318 @@
|
|
|
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 ColorPickerCombo from '@/components/fields/ColorPickerCombo';
|
|
10
|
+
import type { PaneNode } from '@/types/compositorTypes';
|
|
11
|
+
import type { BrandConfig } from '@/types/tractstack';
|
|
12
|
+
|
|
13
|
+
interface FeaturedArticleSetupProps {
|
|
14
|
+
params: Record<string, string>;
|
|
15
|
+
nodeId: string;
|
|
16
|
+
config: BrandConfig;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const comboboxItemStyles = `
|
|
20
|
+
.combo-item .check-indicator {
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
.combo-item[data-state="checked"] .check-indicator {
|
|
24
|
+
display: flex;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const FeaturedArticleSetup = ({
|
|
29
|
+
params,
|
|
30
|
+
nodeId,
|
|
31
|
+
config,
|
|
32
|
+
}: FeaturedArticleSetupProps) => {
|
|
33
|
+
const $contentMap = useStore(fullContentMapStore);
|
|
34
|
+
const $viewportKey = useStore(viewportKeyStore);
|
|
35
|
+
const isInitialMount = useRef(true);
|
|
36
|
+
const ctx = getCtx();
|
|
37
|
+
|
|
38
|
+
const availableStories = useMemo(
|
|
39
|
+
() =>
|
|
40
|
+
$contentMap.filter(
|
|
41
|
+
(item) =>
|
|
42
|
+
item.type === 'StoryFragment' &&
|
|
43
|
+
item.description &&
|
|
44
|
+
item.panes &&
|
|
45
|
+
item.panes.length > 0 &&
|
|
46
|
+
item.thumbSrc
|
|
47
|
+
),
|
|
48
|
+
[$contentMap]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const initialSlug = params?.slug || '';
|
|
52
|
+
const initialStory = availableStories.find(
|
|
53
|
+
(story) => story.slug === initialSlug
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
|
57
|
+
const [selectedSlug, setSelectedSlug] = useState(initialSlug);
|
|
58
|
+
const [query, setQuery] = useState(initialStory?.title || '');
|
|
59
|
+
const [bgColor, setBgColor] = useState(params?.bgColor || '');
|
|
60
|
+
|
|
61
|
+
const selectedStory = useMemo(
|
|
62
|
+
() => availableStories.find((story) => story.slug === selectedSlug),
|
|
63
|
+
[availableStories, selectedSlug]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const collection = useMemo(() => {
|
|
67
|
+
const filtered =
|
|
68
|
+
query === '' || query === selectedStory?.title
|
|
69
|
+
? availableStories
|
|
70
|
+
: availableStories.filter((story) =>
|
|
71
|
+
story.title.toLowerCase().includes(query.toLowerCase())
|
|
72
|
+
);
|
|
73
|
+
return createListCollection({
|
|
74
|
+
items: filtered,
|
|
75
|
+
itemToValue: (item) => item.slug,
|
|
76
|
+
itemToString: (item) => item.title,
|
|
77
|
+
});
|
|
78
|
+
}, [availableStories, query, selectedStory]);
|
|
79
|
+
|
|
80
|
+
const updatePaneNode = () => {
|
|
81
|
+
if (!nodeId) return;
|
|
82
|
+
const allNodes = ctx.allNodes.get();
|
|
83
|
+
const paneNode = cloneDeep(allNodes.get(nodeId)) as PaneNode;
|
|
84
|
+
if (paneNode) {
|
|
85
|
+
const updatedNode = {
|
|
86
|
+
...paneNode,
|
|
87
|
+
codeHookTarget: 'featured-article',
|
|
88
|
+
codeHookPayload: {
|
|
89
|
+
options: JSON.stringify({
|
|
90
|
+
slug: selectedSlug,
|
|
91
|
+
bgColor: bgColor,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
bgColour: bgColor || undefined,
|
|
95
|
+
isChanged: true,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// If bgColor is empty, remove the property
|
|
99
|
+
if (!bgColor) {
|
|
100
|
+
delete updatedNode.bgColour;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ctx.modifyNodes([updatedNode]);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (isInitialMount.current) {
|
|
109
|
+
isInitialMount.current = false;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const timeoutId = setTimeout(updatePaneNode, 500);
|
|
113
|
+
return () => clearTimeout(timeoutId);
|
|
114
|
+
}, [selectedSlug, bgColor]);
|
|
115
|
+
|
|
116
|
+
const handleSelection = (details: { value: string[] }) => {
|
|
117
|
+
const slug = details.value[0] || '';
|
|
118
|
+
setSelectedSlug(slug);
|
|
119
|
+
const story = availableStories.find((s) => s.slug === slug);
|
|
120
|
+
if (story) {
|
|
121
|
+
setQuery(story.title);
|
|
122
|
+
} else {
|
|
123
|
+
setQuery('');
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const renderPreview = () => {
|
|
128
|
+
if (!selectedStory) return null;
|
|
129
|
+
|
|
130
|
+
const topics = selectedStory.topics && selectedStory.topics.length > 0 && (
|
|
131
|
+
<div className="flex flex-wrap gap-2 pt-2">
|
|
132
|
+
{selectedStory.topics.map((topic) => (
|
|
133
|
+
<span
|
|
134
|
+
key={topic}
|
|
135
|
+
className="inline-flex items-center rounded-full bg-cyan-100 px-3 py-1 text-sm font-bold text-cyan-800"
|
|
136
|
+
>
|
|
137
|
+
{topic}
|
|
138
|
+
</span>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Mobile is the default, single-column layout
|
|
144
|
+
if ($viewportKey.value === 'mobile') {
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex flex-col gap-8 pt-4">
|
|
147
|
+
<div className="w-full">
|
|
148
|
+
<p className="font-action text-md mb-4 font-bold uppercase text-gray-500">
|
|
149
|
+
Featured Article
|
|
150
|
+
</p>
|
|
151
|
+
<div className="space-y-6">
|
|
152
|
+
<h2 className="text-4xl font-bold leading-snug">
|
|
153
|
+
{selectedStory.title}
|
|
154
|
+
</h2>
|
|
155
|
+
<p className="text-lg leading-loose text-gray-700">
|
|
156
|
+
{selectedStory.description}
|
|
157
|
+
</p>
|
|
158
|
+
{topics}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="w-full">
|
|
162
|
+
<img
|
|
163
|
+
src={selectedStory.thumbSrc}
|
|
164
|
+
srcSet={selectedStory.thumbSrcSet}
|
|
165
|
+
alt={`Preview of ${selectedStory.title}`}
|
|
166
|
+
className="w-full rounded-lg shadow-lg"
|
|
167
|
+
style={{ aspectRatio: '1200 / 630' }}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Tablet and Desktop share the same two-column layout
|
|
175
|
+
return (
|
|
176
|
+
<div className="flex flex-row items-center gap-12 pt-4">
|
|
177
|
+
<div className="w-3/5">
|
|
178
|
+
<p className="font-action text-md mb-4 font-bold uppercase text-gray-500">
|
|
179
|
+
Featured Article
|
|
180
|
+
</p>
|
|
181
|
+
<div className="space-y-6">
|
|
182
|
+
<h2 className="text-5xl font-bold leading-snug">
|
|
183
|
+
{selectedStory.title}
|
|
184
|
+
</h2>
|
|
185
|
+
<p className="text-lg leading-loose text-gray-700">
|
|
186
|
+
{selectedStory.description}
|
|
187
|
+
</p>
|
|
188
|
+
{topics}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="w-2/5">
|
|
192
|
+
<img
|
|
193
|
+
src={selectedStory.thumbSrc}
|
|
194
|
+
srcSet={selectedStory.thumbSrcSet}
|
|
195
|
+
alt={`Preview of ${selectedStory.title}`}
|
|
196
|
+
className="w-full rounded-lg shadow-lg"
|
|
197
|
+
style={{ aspectRatio: '1200 / 630' }}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (!isPanelOpen) {
|
|
205
|
+
return (
|
|
206
|
+
<div className="flex min-h-[200px] w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
|
|
207
|
+
<button
|
|
208
|
+
onClick={() => setIsPanelOpen(true)}
|
|
209
|
+
className="rounded-lg bg-cyan-600 px-6 py-3 font-bold text-white shadow-md transition-colors hover:bg-cyan-700"
|
|
210
|
+
>
|
|
211
|
+
{selectedStory
|
|
212
|
+
? 'Edit Featured Article'
|
|
213
|
+
: 'Configure Featured Article'}
|
|
214
|
+
</button>
|
|
215
|
+
{selectedStory && (
|
|
216
|
+
<div className="mt-3 text-center text-sm text-gray-600">
|
|
217
|
+
Currently featuring:
|
|
218
|
+
<br />
|
|
219
|
+
<span className="font-bold">{selectedStory.title}</span>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div className="w-full space-y-6 bg-slate-50 p-6">
|
|
228
|
+
<style>{comboboxItemStyles}</style>
|
|
229
|
+
<div className="flex items-center justify-between">
|
|
230
|
+
<h2 className="text-xl font-bold text-gray-900">
|
|
231
|
+
Configure Featured Article
|
|
232
|
+
</h2>
|
|
233
|
+
<button
|
|
234
|
+
onClick={() => setIsPanelOpen(false)}
|
|
235
|
+
className="rounded bg-gray-200 px-4 py-2 font-bold text-gray-800 transition-colors hover:bg-gray-300"
|
|
236
|
+
>
|
|
237
|
+
Close
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div className="rounded-lg bg-white p-4 shadow">
|
|
242
|
+
<label className="block text-sm font-bold text-gray-700">
|
|
243
|
+
Select an Article
|
|
244
|
+
</label>
|
|
245
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
246
|
+
Only articles with a description, content, and thumbnail will be
|
|
247
|
+
shown.
|
|
248
|
+
</p>
|
|
249
|
+
<Combobox.Root
|
|
250
|
+
collection={collection}
|
|
251
|
+
value={selectedSlug ? [selectedSlug] : []}
|
|
252
|
+
inputValue={query}
|
|
253
|
+
onValueChange={handleSelection}
|
|
254
|
+
onInputValueChange={(details) => setQuery(details.inputValue)}
|
|
255
|
+
className="mt-2"
|
|
256
|
+
>
|
|
257
|
+
<div className="relative">
|
|
258
|
+
<Combobox.Input
|
|
259
|
+
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"
|
|
260
|
+
placeholder="Search for an article..."
|
|
261
|
+
/>
|
|
262
|
+
<Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
263
|
+
<ChevronUpDownIcon
|
|
264
|
+
className="h-5 w-5 text-gray-400"
|
|
265
|
+
aria-hidden="true"
|
|
266
|
+
/>
|
|
267
|
+
</Combobox.Trigger>
|
|
268
|
+
</div>
|
|
269
|
+
<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">
|
|
270
|
+
{collection.items.map((item) => (
|
|
271
|
+
<Combobox.Item
|
|
272
|
+
key={item.slug}
|
|
273
|
+
item={item}
|
|
274
|
+
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"
|
|
275
|
+
>
|
|
276
|
+
<span className="block truncate">{item.title}</span>
|
|
277
|
+
<span className="check-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
|
|
278
|
+
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
279
|
+
</span>
|
|
280
|
+
</Combobox.Item>
|
|
281
|
+
))}
|
|
282
|
+
</Combobox.Content>
|
|
283
|
+
</Combobox.Root>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div className="rounded-lg bg-white p-4 shadow">
|
|
287
|
+
<div className="border-b border-gray-200 pb-4">
|
|
288
|
+
<h3 className="text-lg font-bold text-gray-900">Display Settings</h3>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="space-y-4 pt-4">
|
|
291
|
+
<div>
|
|
292
|
+
<ColorPickerCombo
|
|
293
|
+
title="Background Color"
|
|
294
|
+
defaultColor={bgColor}
|
|
295
|
+
onColorChange={(color: string) => setBgColor(color)}
|
|
296
|
+
config={config!}
|
|
297
|
+
allowNull={true}
|
|
298
|
+
/>
|
|
299
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
300
|
+
Set a background color for the featured article section
|
|
301
|
+
</p>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{selectedStory && (
|
|
307
|
+
<div className="rounded-lg bg-white p-4 shadow">
|
|
308
|
+
<h3 className="border-b border-gray-200 pb-2 text-lg font-bold">
|
|
309
|
+
Live Preview
|
|
310
|
+
</h3>
|
|
311
|
+
{renderPreview()}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export default FeaturedArticleSetup;
|
|
@@ -22,6 +22,8 @@ try {
|
|
|
22
22
|
topics: '',
|
|
23
23
|
excludedIds: '',
|
|
24
24
|
pageSize: 10,
|
|
25
|
+
title: '',
|
|
26
|
+
bgColor: '',
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -30,6 +32,8 @@ const excludedIdsArray = parsedOptions.excludedIds
|
|
|
30
32
|
: [];
|
|
31
33
|
const topicsArray = parsedOptions.topics ? parsedOptions.topics.split(',') : [];
|
|
32
34
|
const pageSize = parseInt(parsedOptions.pageSize || '10');
|
|
35
|
+
const title = parsedOptions.title;
|
|
36
|
+
const bgColor = parsedOptions.bgColor || '';
|
|
33
37
|
|
|
34
38
|
// Filter for valid stories to display
|
|
35
39
|
const validPages = contentMap.filter(
|
|
@@ -52,50 +56,25 @@ if (topicsArray.length > 0) {
|
|
|
52
56
|
filteredStories = validPages;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
59
|
+
// Simple sort by date - most recent first
|
|
60
|
+
const sortedStories = [...filteredStories].sort((a, b) => {
|
|
57
61
|
const dateA = a.changed ? new Date(a.changed).getTime() : 0;
|
|
58
62
|
const dateB = b.changed ? new Date(b.changed).getTime() : 0;
|
|
59
63
|
return dateB - dateA;
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
// The initial display
|
|
63
|
-
const initialStories =
|
|
64
|
-
const totalPages = Math.ceil(
|
|
65
|
-
|
|
66
|
-
// Date formatting helper
|
|
67
|
-
function formatDate(dateString: string | null): string {
|
|
68
|
-
if (!dateString) return 'Unknown';
|
|
69
|
-
const date = new Date(dateString);
|
|
70
|
-
return new Intl.DateTimeFormat('en-US', {
|
|
71
|
-
year: 'numeric',
|
|
72
|
-
month: 'long',
|
|
73
|
-
day: 'numeric',
|
|
74
|
-
}).format(date);
|
|
75
|
-
}
|
|
66
|
+
// The initial display
|
|
67
|
+
const initialStories = sortedStories.slice(0, pageSize);
|
|
68
|
+
const totalPages = Math.ceil(sortedStories.length / pageSize);
|
|
76
69
|
---
|
|
77
70
|
|
|
78
|
-
<div
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<button
|
|
86
|
-
id="recent-toggle"
|
|
87
|
-
class="rounded-l-md border border-cyan-600 bg-cyan-600 px-4 py-2 text-sm font-bold text-white transition-colors"
|
|
88
|
-
>
|
|
89
|
-
Newest
|
|
90
|
-
</button>
|
|
91
|
-
<button
|
|
92
|
-
id="popular-toggle"
|
|
93
|
-
class="rounded-r-md border border-cyan-600 bg-white px-4 py-2 text-sm font-bold text-gray-800 transition-colors hover:bg-gray-100"
|
|
94
|
-
>
|
|
95
|
-
Most Active
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
71
|
+
<div
|
|
72
|
+
class="mx-auto max-w-7xl p-4 py-24"
|
|
73
|
+
style={bgColor ? `background-color: ${bgColor}` : ''}
|
|
74
|
+
>
|
|
75
|
+
<h2 class="mb-8 text-center text-3xl font-bold text-gray-900">
|
|
76
|
+
{title || `Recent Articles`}
|
|
77
|
+
</h2>
|
|
99
78
|
|
|
100
79
|
{
|
|
101
80
|
initialStories.length === 0 && (
|
|
@@ -113,9 +92,10 @@ function formatDate(dateString: string | null): string {
|
|
|
113
92
|
{story.thumbSrc && (
|
|
114
93
|
<img
|
|
115
94
|
src={story.thumbSrc}
|
|
95
|
+
srcset={story.thumbSrcSet}
|
|
116
96
|
alt={story.title}
|
|
117
|
-
style="
|
|
118
|
-
class="rounded-md"
|
|
97
|
+
style="aspect-ratio: 1200 / 630;"
|
|
98
|
+
class="w-36 flex-shrink-0 rounded-md md:w-48 xl:w-72"
|
|
119
99
|
/>
|
|
120
100
|
)}
|
|
121
101
|
<div class="flex-1">
|
|
@@ -128,7 +108,7 @@ function formatDate(dateString: string | null): string {
|
|
|
128
108
|
</p>
|
|
129
109
|
)}
|
|
130
110
|
{story.topics && story.topics.length > 0 && (
|
|
131
|
-
<div class="mt-
|
|
111
|
+
<div class="mt-4 flex flex-wrap gap-1">
|
|
132
112
|
{story.topics.slice(0, 3).map((topic: string) => (
|
|
133
113
|
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
|
|
134
114
|
{topic}
|
|
@@ -136,9 +116,6 @@ function formatDate(dateString: string | null): string {
|
|
|
136
116
|
))}
|
|
137
117
|
</div>
|
|
138
118
|
)}
|
|
139
|
-
<p class="mt-1 text-xs text-gray-600">
|
|
140
|
-
{story.changed && formatDate(story.changed)}
|
|
141
|
-
</p>
|
|
142
119
|
</div>
|
|
143
120
|
</div>
|
|
144
121
|
</a>
|
|
@@ -157,9 +134,10 @@ function formatDate(dateString: string | null): string {
|
|
|
157
134
|
{story.thumbSrc && (
|
|
158
135
|
<img
|
|
159
136
|
src={story.thumbSrc}
|
|
137
|
+
srcset={story.thumbSrcSet}
|
|
160
138
|
alt={story.title}
|
|
161
|
-
style="
|
|
162
|
-
class="rounded-md"
|
|
139
|
+
style="aspect-ratio: 1200 / 630;"
|
|
140
|
+
class="w-36 flex-shrink-0 rounded-md md:w-48 xl:w-72"
|
|
163
141
|
/>
|
|
164
142
|
)}
|
|
165
143
|
<div class="flex-1">
|
|
@@ -180,9 +158,6 @@ function formatDate(dateString: string | null): string {
|
|
|
180
158
|
))}
|
|
181
159
|
</div>
|
|
182
160
|
)}
|
|
183
|
-
<p class="mt-1 text-xs text-gray-600">
|
|
184
|
-
{story.changed && formatDate(story.changed)}
|
|
185
|
-
</p>
|
|
186
161
|
</div>
|
|
187
162
|
</div>
|
|
188
163
|
</a>
|
|
@@ -199,9 +174,10 @@ function formatDate(dateString: string | null): string {
|
|
|
199
174
|
{story.thumbSrc && (
|
|
200
175
|
<img
|
|
201
176
|
src={story.thumbSrc}
|
|
177
|
+
srcset={story.thumbSrcSet}
|
|
202
178
|
alt={story.title}
|
|
203
|
-
style="
|
|
204
|
-
class="rounded-md"
|
|
179
|
+
style="aspect-ratio: 1200 / 630;"
|
|
180
|
+
class="w-36 flex-shrink-0 rounded-md md:w-48 xl:w-72"
|
|
205
181
|
/>
|
|
206
182
|
)}
|
|
207
183
|
<div class="flex-1">
|
|
@@ -222,9 +198,6 @@ function formatDate(dateString: string | null): string {
|
|
|
222
198
|
))}
|
|
223
199
|
</div>
|
|
224
200
|
)}
|
|
225
|
-
<p class="mt-1 text-xs text-gray-600">
|
|
226
|
-
{story.changed && formatDate(story.changed)}
|
|
227
|
-
</p>
|
|
228
201
|
</div>
|
|
229
202
|
</div>
|
|
230
203
|
</a>
|
|
@@ -264,100 +237,30 @@ function formatDate(dateString: string | null): string {
|
|
|
264
237
|
<script
|
|
265
238
|
is:inline
|
|
266
239
|
define:vars={{
|
|
267
|
-
|
|
240
|
+
sortedStories,
|
|
268
241
|
pageSize,
|
|
269
242
|
}}
|
|
270
243
|
>
|
|
271
244
|
// === CLIENT-SIDE STATE ===
|
|
272
245
|
let currentPage = 1;
|
|
273
|
-
let currentMode = 'recent'; // Default mode is always 'recent' on initial load
|
|
274
|
-
let sortedByPopular = []; // This will be populated by the fetch function
|
|
275
246
|
|
|
276
247
|
// === DOM ELEMENTS ===
|
|
277
248
|
const mobileContainer = document.getElementById('mobile-content');
|
|
278
249
|
const desktopContainer = document.getElementById('desktop-content');
|
|
279
|
-
const toggleContainer = document.getElementById('toggle-container');
|
|
280
|
-
const recentToggle = document.getElementById('recent-toggle');
|
|
281
|
-
const popularToggle = document.getElementById('popular-toggle');
|
|
282
250
|
const prevPageBtn = document.getElementById('prev-page');
|
|
283
251
|
const nextPageBtn = document.getElementById('next-page');
|
|
284
252
|
|
|
285
|
-
// === DATA FETCHING & PROCESSING ===
|
|
286
|
-
async function fetchHotContent() {
|
|
287
|
-
let retryCount = 0;
|
|
288
|
-
const maxRetries = 2;
|
|
289
|
-
|
|
290
|
-
const attemptFetch = async () => {
|
|
291
|
-
try {
|
|
292
|
-
const goBackend =
|
|
293
|
-
window.location.protocol + '//' + window.location.host;
|
|
294
|
-
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
295
|
-
const response = await fetch(
|
|
296
|
-
`${goBackend}/api/v1/analytics/content-summary`,
|
|
297
|
-
{
|
|
298
|
-
method: 'GET',
|
|
299
|
-
headers: {
|
|
300
|
-
'Content-Type': 'application/json',
|
|
301
|
-
'X-Tenant-ID': tenantId,
|
|
302
|
-
},
|
|
303
|
-
}
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
|
|
307
|
-
|
|
308
|
-
const data = await response.json();
|
|
309
|
-
|
|
310
|
-
if (data.hotContent && data.hotContent.length > 0) {
|
|
311
|
-
// On success, create the popular sort array
|
|
312
|
-
const viewsMap = new Map(
|
|
313
|
-
data.hotContent.map((item) => [item.id, item.totalEvents])
|
|
314
|
-
);
|
|
315
|
-
sortedByPopular = [...sortedByRecent].sort((a, b) => {
|
|
316
|
-
const aViews = viewsMap.get(a.id) || 0;
|
|
317
|
-
const bViews = viewsMap.get(b.id) || 0;
|
|
318
|
-
if (bViews === aViews) {
|
|
319
|
-
const dateA = a.changed ? new Date(a.changed).getTime() : 0;
|
|
320
|
-
const dateB = b.changed ? new Date(b.changed).getTime() : 0;
|
|
321
|
-
return dateB - dateA;
|
|
322
|
-
}
|
|
323
|
-
return bViews - aViews;
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// Show the toggle buttons
|
|
327
|
-
if (toggleContainer) toggleContainer.style.display = 'flex';
|
|
328
|
-
} else if (retryCount < maxRetries) {
|
|
329
|
-
retryCount++;
|
|
330
|
-
setTimeout(attemptFetch, retryCount * 3000);
|
|
331
|
-
}
|
|
332
|
-
} catch (error) {
|
|
333
|
-
console.warn('Could not fetch hot content:', error);
|
|
334
|
-
if (retryCount < maxRetries) {
|
|
335
|
-
retryCount++;
|
|
336
|
-
setTimeout(attemptFetch, retryCount * 3000);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
await attemptFetch();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
253
|
// === DOM UPDATING ===
|
|
344
254
|
function createStoryItem(story) {
|
|
345
|
-
const formattedDate = story.changed
|
|
346
|
-
? new Intl.DateTimeFormat('en-US', {
|
|
347
|
-
year: 'numeric',
|
|
348
|
-
month: 'long',
|
|
349
|
-
day: 'numeric',
|
|
350
|
-
}).format(new Date(story.changed))
|
|
351
|
-
: 'Unknown';
|
|
352
255
|
const imageHtml = story.thumbSrc
|
|
353
|
-
? `<img src="${story.thumbSrc}" alt="${story.title}" style="
|
|
256
|
+
? `<img src="${story.thumbSrc}" srcset="${story.thumbSrcSet || ''}" alt="${story.title}" style="aspect-ratio: 1200 / 630;" class="w-36 flex-shrink-0 rounded-md md:w-48 xl:w-72">`
|
|
354
257
|
: '';
|
|
355
258
|
const descriptionHtml = story.description
|
|
356
259
|
? `<p class="text-sm text-gray-800 line-clamp-2">${story.description}</p>`
|
|
357
260
|
: '';
|
|
358
261
|
const topicsHtml =
|
|
359
262
|
story.topics && story.topics.length > 0
|
|
360
|
-
? `<div class="mt-
|
|
263
|
+
? `<div class="mt-4 flex flex-wrap gap-1">${story.topics
|
|
361
264
|
.slice(0, 3)
|
|
362
265
|
.map(
|
|
363
266
|
(topic) =>
|
|
@@ -374,7 +277,6 @@ function formatDate(dateString: string | null): string {
|
|
|
374
277
|
<h3 class="text-lg font-bold text-black transition-colors group-hover:text-gray-900">${story.title}</h3>
|
|
375
278
|
${descriptionHtml}
|
|
376
279
|
${topicsHtml}
|
|
377
|
-
<p class="mt-1 text-xs text-gray-600">${formattedDate}</p>
|
|
378
280
|
</div>
|
|
379
281
|
</div>
|
|
380
282
|
</a>
|
|
@@ -382,13 +284,10 @@ function formatDate(dateString: string | null): string {
|
|
|
382
284
|
}
|
|
383
285
|
|
|
384
286
|
function updateDisplayedContent() {
|
|
385
|
-
const
|
|
386
|
-
currentMode === 'recent' ? sortedByRecent : sortedByPopular;
|
|
387
|
-
const totalPages = Math.ceil(currentData.length / pageSize);
|
|
388
|
-
|
|
287
|
+
const totalPages = Math.ceil(sortedStories.length / pageSize);
|
|
389
288
|
const startIdx = (currentPage - 1) * pageSize;
|
|
390
289
|
const endIdx = startIdx + pageSize;
|
|
391
|
-
const pageData =
|
|
290
|
+
const pageData = sortedStories.slice(startIdx, endIdx);
|
|
392
291
|
|
|
393
292
|
if (mobileContainer)
|
|
394
293
|
mobileContainer.innerHTML = pageData.map(createStoryItem).join('');
|
|
@@ -413,28 +312,6 @@ function formatDate(dateString: string | null): string {
|
|
|
413
312
|
}
|
|
414
313
|
|
|
415
314
|
// === EVENT LISTENERS ===
|
|
416
|
-
function switchMode(newMode) {
|
|
417
|
-
if (currentMode === newMode) return;
|
|
418
|
-
currentMode = newMode;
|
|
419
|
-
currentPage = 1;
|
|
420
|
-
|
|
421
|
-
recentToggle.classList.toggle('bg-cyan-600', newMode === 'recent');
|
|
422
|
-
recentToggle.classList.toggle('text-white', newMode === 'recent');
|
|
423
|
-
recentToggle.classList.toggle('bg-white', newMode !== 'recent');
|
|
424
|
-
recentToggle.classList.toggle('text-gray-800', newMode !== 'recent');
|
|
425
|
-
|
|
426
|
-
popularToggle.classList.toggle('bg-cyan-600', newMode === 'popular');
|
|
427
|
-
popularToggle.classList.toggle('text-white', newMode === 'popular');
|
|
428
|
-
popularToggle.classList.toggle('bg-white', newMode !== 'popular');
|
|
429
|
-
popularToggle.classList.toggle('text-gray-800', newMode !== 'popular');
|
|
430
|
-
|
|
431
|
-
updateDisplayedContent();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (recentToggle)
|
|
435
|
-
recentToggle.addEventListener('click', () => switchMode('recent'));
|
|
436
|
-
if (popularToggle)
|
|
437
|
-
popularToggle.addEventListener('click', () => switchMode('popular'));
|
|
438
315
|
if (prevPageBtn)
|
|
439
316
|
prevPageBtn.addEventListener('click', () => {
|
|
440
317
|
if (currentPage > 1) {
|
|
@@ -444,17 +321,10 @@ function formatDate(dateString: string | null): string {
|
|
|
444
321
|
});
|
|
445
322
|
if (nextPageBtn)
|
|
446
323
|
nextPageBtn.addEventListener('click', () => {
|
|
447
|
-
const
|
|
448
|
-
currentMode === 'recent' ? sortedByRecent : sortedByPopular;
|
|
449
|
-
const totalPages = Math.ceil(currentData.length / pageSize);
|
|
324
|
+
const totalPages = Math.ceil(sortedStories.length / pageSize);
|
|
450
325
|
if (currentPage < totalPages) {
|
|
451
326
|
currentPage++;
|
|
452
327
|
updateDisplayedContent();
|
|
453
328
|
}
|
|
454
329
|
});
|
|
455
|
-
|
|
456
|
-
// === INITIALIZATION ===
|
|
457
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
458
|
-
fetchHotContent();
|
|
459
|
-
});
|
|
460
330
|
</script>
|