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.
Files changed (142) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +31 -8
  4. package/dist/index.js +106 -29
  5. package/package.json +10 -5
  6. package/templates/css/frontend.css +1 -1
  7. package/templates/custom/minimal/CodeHook.astro +13 -12
  8. package/templates/custom/minimal/CustomRoutes.astro +25 -31
  9. package/templates/custom/with-examples/CodeHook.astro +22 -11
  10. package/templates/custom/with-examples/CustomRoutes.astro +4 -8
  11. package/templates/custom/with-examples/ProductCard.astro +29 -0
  12. package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
  13. package/templates/custom/with-examples/ProductGrid.astro +64 -0
  14. package/templates/custom/with-examples/pages/Collections.astro +58 -98
  15. package/templates/gitignore +42 -0
  16. package/templates/prettierignore +5 -0
  17. package/templates/prettierrc +19 -0
  18. package/templates/src/client/app.js +127 -0
  19. package/templates/src/client/htmx.min.js +3519 -0
  20. package/templates/src/client/view.js +429 -0
  21. package/templates/src/components/Footer.astro +4 -9
  22. package/templates/src/components/Header.astro +67 -60
  23. package/templates/src/components/Menu.tsx +188 -52
  24. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  25. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
  26. package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
  27. package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -9
  28. package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
  29. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
  30. package/templates/src/components/codehooks/ListContent.astro +32 -162
  31. package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
  32. package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
  33. package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
  34. package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
  35. package/templates/src/components/compositor/Node.tsx +3 -6
  36. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
  37. package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
  38. package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
  39. package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
  40. package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
  41. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
  42. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
  43. package/templates/src/components/edit/Header.tsx +10 -4
  44. package/templates/src/components/edit/PanelSwitch.tsx +11 -7
  45. package/templates/src/components/edit/SettingsPanel.tsx +29 -18
  46. package/templates/src/components/edit/ToolBar.tsx +1 -28
  47. package/templates/src/components/edit/ToolMode.tsx +45 -32
  48. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
  49. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
  50. package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
  51. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
  52. package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
  53. package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
  54. package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
  55. package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
  56. package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
  57. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
  58. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
  59. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
  60. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
  61. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
  62. package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
  63. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
  64. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
  66. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
  67. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
  68. package/templates/src/components/edit/state/SaveModal.tsx +316 -169
  69. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
  70. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
  71. package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
  72. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
  73. package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
  74. package/templates/src/components/fields/ArtpackImage.tsx +4 -1
  75. package/templates/src/components/fields/BackgroundImage.tsx +1 -1
  76. package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
  77. package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
  78. package/templates/src/components/fields/ImageUpload.tsx +1 -1
  79. package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
  80. package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
  81. package/templates/src/components/form/ActionBuilderField.tsx +306 -87
  82. package/templates/src/components/search/SearchModal.tsx +420 -0
  83. package/templates/src/components/search/SearchResults.tsx +367 -0
  84. package/templates/src/components/search/SearchWrapper.tsx +46 -0
  85. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
  86. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
  87. package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
  88. package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
  89. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  90. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
  91. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
  92. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
  93. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
  94. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  95. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  96. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  97. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  98. package/templates/src/constants/shapes.ts +9 -0
  99. package/templates/src/constants.ts +2121 -16
  100. package/templates/src/hooks/useSearch.ts +228 -0
  101. package/templates/src/layouts/Layout.astro +213 -104
  102. package/templates/src/lib/storyData.ts +4 -1
  103. package/templates/src/pages/[...slug]/edit.astro +14 -14
  104. package/templates/src/pages/[...slug].astro +82 -21
  105. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  106. package/templates/src/pages/api/tailwind.ts +23 -21
  107. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  108. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  109. package/templates/src/pages/storykeep/advanced.astro +5 -4
  110. package/templates/src/pages/storykeep/branding.astro +5 -4
  111. package/templates/src/pages/storykeep/content.astro +5 -4
  112. package/templates/src/pages/storykeep/init.astro +40 -1
  113. package/templates/src/pages/storykeep/login.astro +1 -1
  114. package/templates/src/pages/storykeep.astro +5 -4
  115. package/templates/src/stores/nodes.ts +59 -88
  116. package/templates/src/stores/orphanAnalysis.ts +19 -21
  117. package/templates/src/stores/storykeep.ts +7 -0
  118. package/templates/src/types/compositorTypes.ts +6 -0
  119. package/templates/src/types/tractstack.ts +17 -0
  120. package/templates/src/utils/actions/lispLexer.ts +2 -2
  121. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  122. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  123. package/templates/src/utils/api/menuHelpers.ts +2 -2
  124. package/templates/src/utils/api.ts +26 -0
  125. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  126. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  127. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  128. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  129. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  130. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  131. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  132. package/templates/src/utils/customHelpers.ts +38 -0
  133. package/templates/src/utils/helpers.ts +2 -2
  134. package/templates/src/utils/layout.ts +65 -144
  135. package/utils/inject-files.ts +95 -18
  136. package/templates/src/client/analytics-events.js +0 -207
  137. package/templates/src/client/belief-events.js +0 -191
  138. package/templates/src/client/sse.js +0 -613
  139. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  140. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  141. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  142. 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
- // The server now ONLY sorts by recent. 'Popular' sort is handled on the client.
56
- const sortedByRecent = [...filteredStories].sort((a, b) => {
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 will always be the most recent stories.
63
- const initialStories = sortedByRecent.slice(0, pageSize);
64
- const totalPages = Math.ceil(sortedByRecent.length / pageSize);
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 class="mx-auto max-w-7xl p-4 py-12">
79
- <div
80
- id="toggle-container"
81
- class="mb-4 flex justify-center"
82
- style="display: none;"
83
- >
84
- <div class="inline-flex rounded-md shadow-sm" role="group">
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="width: 100px; height: auto;"
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-1 flex flex-wrap gap-1">
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="width: 100px; height: auto;"
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="width: 100px; height: auto;"
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
- sortedByRecent,
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="width: 100px; height: auto;" class="rounded-md">`
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-1 flex flex-wrap gap-1">${story.topics
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 currentData =
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 = currentData.slice(startIdx, endIdx);
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 currentData =
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>