astro-tractstack 2.0.0-rc.8 → 2.0.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 (141) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +35 -11
  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 +1 -0
  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_Content.tsx +6 -0
  88. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  89. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
  90. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
  91. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
  92. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  93. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  94. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  95. package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
  96. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  97. package/templates/src/constants/shapes.ts +9 -0
  98. package/templates/src/constants.ts +2121 -16
  99. package/templates/src/hooks/useSearch.ts +228 -0
  100. package/templates/src/layouts/Layout.astro +213 -104
  101. package/templates/src/lib/storyData.ts +4 -1
  102. package/templates/src/pages/[...slug]/edit.astro +14 -14
  103. package/templates/src/pages/[...slug].astro +82 -21
  104. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  105. package/templates/src/pages/api/tailwind.ts +23 -21
  106. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  107. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  108. package/templates/src/pages/storykeep/advanced.astro +5 -4
  109. package/templates/src/pages/storykeep/branding.astro +5 -4
  110. package/templates/src/pages/storykeep/content.astro +5 -4
  111. package/templates/src/pages/storykeep/init.astro +40 -1
  112. package/templates/src/pages/storykeep/login.astro +1 -1
  113. package/templates/src/pages/storykeep.astro +5 -4
  114. package/templates/src/stores/nodes.ts +59 -88
  115. package/templates/src/stores/orphanAnalysis.ts +19 -21
  116. package/templates/src/stores/storykeep.ts +7 -0
  117. package/templates/src/types/compositorTypes.ts +6 -0
  118. package/templates/src/types/tractstack.ts +17 -0
  119. package/templates/src/utils/actions/lispLexer.ts +2 -2
  120. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  121. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  122. package/templates/src/utils/api/menuHelpers.ts +2 -2
  123. package/templates/src/utils/api.ts +26 -0
  124. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  125. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  126. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  127. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  128. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  129. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  130. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  131. package/templates/src/utils/customHelpers.ts +38 -0
  132. package/templates/src/utils/helpers.ts +2 -2
  133. package/templates/src/utils/layout.ts +65 -144
  134. package/utils/inject-files.ts +95 -18
  135. package/templates/src/client/analytics-events.js +0 -207
  136. package/templates/src/client/belief-events.js +0 -191
  137. package/templates/src/client/sse.js +0 -613
  138. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  139. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  140. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  141. package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
@@ -2,7 +2,8 @@ import { Menu } from '@ark-ui/react';
2
2
  import { Portal } from '@ark-ui/react/portal';
3
3
  import ChevronDownIcon from '@heroicons/react/20/solid/ChevronDownIcon';
4
4
  import { lispLexer } from '@/utils/actions/lispLexer';
5
- import { preParseAction } from '@/utils/actions//preParse_Action';
5
+ import { preParseAction } from '@/utils/actions/preParse_Action';
6
+ import type { LispToken } from '@/types/compositorTypes';
6
7
 
7
8
  // CSS to style the menu items with hover and selection states
8
9
  const menuStyles = `
@@ -46,9 +47,10 @@ interface MenuDatum {
46
47
  optionsPayload: MenuLink[];
47
48
  }
48
49
 
49
- interface MenuLinkDatum extends MenuLink {
50
- to: string;
51
- internal: boolean;
50
+ interface ProcessedMenuLinkDatum extends MenuLink {
51
+ renderAs: 'a' | 'button' | 'span';
52
+ href?: string;
53
+ htmxVals?: string;
52
54
  }
53
55
 
54
56
  interface MenuProps {
@@ -62,7 +64,75 @@ const MenuComponent = (props: MenuProps) => {
62
64
  const { payload, slug, isContext, brandConfig } = props;
63
65
  const thisPayload = payload.optionsPayload;
64
66
 
65
- // Process featured and additional links
67
+ function processMenuLink(e: MenuLink): ProcessedMenuLinkDatum {
68
+ const item = { ...e } as ProcessedMenuLinkDatum;
69
+ const actionLisp = item.actionLisp?.trim();
70
+
71
+ if (!actionLisp) {
72
+ item.renderAs = 'span';
73
+ return item;
74
+ }
75
+
76
+ try {
77
+ if (actionLisp.startsWith('(goto')) {
78
+ const tokens = lispLexer(actionLisp);
79
+ const to = preParseAction(tokens, slug, isContext, brandConfig);
80
+ item.renderAs = 'a';
81
+ item.href = to || '#';
82
+ return item;
83
+ }
84
+
85
+ const [lispTokens] = lispLexer(actionLisp);
86
+
87
+ if (lispTokens && lispTokens.length > 0) {
88
+ // Deconstruct the nested structure: e.g., ['declare', ['HotLead', 'BELIEVES_YES']]
89
+ const tokens = lispTokens[0] as LispToken[];
90
+
91
+ if (
92
+ (tokens[0] === 'declare' || tokens[0] === 'identifyAs') &&
93
+ Array.isArray(tokens[1]) &&
94
+ tokens[1].length >= 2
95
+ ) {
96
+ const command = tokens[0] as string;
97
+ const params = tokens[1] as (string | number)[];
98
+ const beliefId = params[0] as string;
99
+ const value = params[1] as string;
100
+
101
+ let hxValsMap: { [key: string]: string } = {};
102
+
103
+ if (command === 'declare') {
104
+ hxValsMap = {
105
+ beliefId: beliefId,
106
+ beliefType: 'Belief',
107
+ beliefValue: value,
108
+ };
109
+ } else if (command === 'identifyAs') {
110
+ hxValsMap = {
111
+ beliefId: beliefId,
112
+ beliefType: 'Belief',
113
+ beliefVerb: 'IDENTIFY_AS',
114
+ beliefObject: value,
115
+ };
116
+ }
117
+
118
+ if (Object.keys(hxValsMap).length > 0) {
119
+ item.renderAs = 'button';
120
+ item.htmxVals = JSON.stringify(hxValsMap);
121
+ return item;
122
+ }
123
+ }
124
+ }
125
+ } catch (error) {
126
+ console.error(
127
+ `Failed to process menu item for action: ${actionLisp}`,
128
+ error
129
+ );
130
+ }
131
+
132
+ item.renderAs = 'span';
133
+ return item;
134
+ }
135
+
66
136
  const featuredLinks = thisPayload
67
137
  .filter((e: MenuLink) => e.featured)
68
138
  .map(processMenuLink);
@@ -70,19 +140,46 @@ const MenuComponent = (props: MenuProps) => {
70
140
  .filter((e: MenuLink) => !e.featured)
71
141
  .map(processMenuLink);
72
142
 
73
- // Helper function to process menu links
74
- function processMenuLink(e: MenuLink): MenuLinkDatum {
75
- const item = { ...e } as MenuLinkDatum;
76
- const thisPayload = lispLexer(e.actionLisp);
77
- const to = preParseAction(thisPayload, slug, isContext, brandConfig);
78
- if (typeof to === `string`) {
79
- item.to = to;
80
- item.internal = true;
81
- } else if (typeof to === `object`) {
82
- item.to = to[0];
143
+ const InteractiveMenuItem = ({ item }: { item: ProcessedMenuLinkDatum }) => {
144
+ if (item.renderAs === 'button') {
145
+ return (
146
+ <button
147
+ type="button"
148
+ className="text-mydarkgrey focus:ring-myblue block text-2xl font-bold leading-6 hover:text-black hover:underline hover:decoration-dashed hover:decoration-4 hover:underline-offset-4 focus:text-black focus:outline-none focus:ring-2"
149
+ title={item.description}
150
+ aria-label={`${item.name} - ${item.description}`}
151
+ hx-post="/api/v1/state"
152
+ hx-swap="none"
153
+ hx-vals={item.htmxVals}
154
+ >
155
+ {item.name}
156
+ </button>
157
+ );
158
+ }
159
+
160
+ if (item.renderAs === 'a') {
161
+ return (
162
+ <a
163
+ href={item.href}
164
+ className="text-mydarkgrey focus:ring-myblue block text-2xl font-bold leading-6 hover:text-black hover:underline hover:decoration-dashed hover:decoration-4 hover:underline-offset-4 focus:text-black focus:outline-none focus:ring-2"
165
+ title={item.description}
166
+ aria-label={`${item.name} - ${item.description}`}
167
+ >
168
+ {item.name}
169
+ </a>
170
+ );
83
171
  }
84
- return item;
85
- }
172
+
173
+ return (
174
+ <span
175
+ className="text-mydarkgrey block text-2xl font-bold leading-6 opacity-50"
176
+ title={item.description}
177
+ aria-label={`${item.name} - ${item.description}`}
178
+ >
179
+ {item.name}
180
+ </span>
181
+ );
182
+ };
86
183
 
87
184
  return (
88
185
  <>
@@ -90,16 +187,9 @@ const MenuComponent = (props: MenuProps) => {
90
187
 
91
188
  {/* Desktop Navigation */}
92
189
  <nav className="font-action ml-6 hidden flex-wrap items-center justify-end space-x-3 md:flex md:space-x-6">
93
- {featuredLinks.map((item: MenuLinkDatum) => (
190
+ {featuredLinks.map((item: ProcessedMenuLinkDatum) => (
94
191
  <div key={item.name} className="relative py-1.5">
95
- <a
96
- href={item.to}
97
- className="text-mydarkgrey focus:ring-myblue block text-2xl font-bold leading-6 hover:text-black hover:underline hover:decoration-dashed hover:decoration-4 hover:underline-offset-4 focus:text-black focus:outline-none focus:ring-2"
98
- title={item.description}
99
- aria-label={`${item.name} - ${item.description}`}
100
- >
101
- {item.name}
102
- </a>
192
+ <InteractiveMenuItem item={item} />
103
193
  </div>
104
194
  ))}
105
195
  </nav>
@@ -122,21 +212,42 @@ const MenuComponent = (props: MenuProps) => {
122
212
  <div className="text-md ring-mydarkgrey/5 flex-auto overflow-hidden rounded-3xl bg-white p-4 leading-6 shadow-lg ring-1">
123
213
  {/* Featured Links Section */}
124
214
  <div className="px-8">
125
- {featuredLinks.map((item: MenuLinkDatum) => (
215
+ {featuredLinks.map((item: ProcessedMenuLinkDatum) => (
126
216
  <Menu.Item
127
217
  key={item.name}
128
218
  value={item.name}
129
219
  className="menu-item hover:bg-mygreen/20 group relative flex gap-x-6 rounded-lg p-4"
130
220
  >
131
221
  <div>
132
- <a
133
- href={item.to}
134
- className="font-action text-myblack text-xl hover:text-black focus:text-black focus:outline-none"
135
- aria-label={`${item.name} - ${item.description}`}
136
- >
137
- {item.name}
138
- <span className="absolute inset-0" />
139
- </a>
222
+ {item.renderAs === 'button' ? (
223
+ <button
224
+ type="button"
225
+ className="font-action text-myblack text-xl hover:text-black focus:text-black focus:outline-none"
226
+ aria-label={`${item.name} - ${item.description}`}
227
+ hx-post="/api/v1/state"
228
+ hx-swap="none"
229
+ hx-vals={item.htmxVals}
230
+ >
231
+ {item.name}
232
+ <span className="absolute inset-0" />
233
+ </button>
234
+ ) : item.renderAs === 'a' ? (
235
+ <a
236
+ href={item.href}
237
+ className="font-action text-myblack text-xl hover:text-black focus:text-black focus:outline-none"
238
+ aria-label={`${item.name} - ${item.description}`}
239
+ >
240
+ {item.name}
241
+ <span className="absolute inset-0" />
242
+ </a>
243
+ ) : (
244
+ <span
245
+ className="font-action text-myblack text-xl opacity-50"
246
+ aria-label={`${item.name} - ${item.description}`}
247
+ >
248
+ {item.name}
249
+ </span>
250
+ )}
140
251
  <p className="text-mydarkgrey mt-1">
141
252
  {item.description}
142
253
  </p>
@@ -161,24 +272,49 @@ const MenuComponent = (props: MenuProps) => {
161
272
  className="mt-6 space-y-6"
162
273
  aria-labelledby="additional-links-heading"
163
274
  >
164
- {additionalLinks.map((item: MenuLinkDatum) => (
165
- <li key={item.name} className="relative">
166
- <Menu.Item
167
- value={item.name}
168
- className="menu-item block w-full text-left"
169
- >
170
- <a
171
- href={item.to}
172
- className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 hover:text-black focus:text-black focus:underline focus:outline-none"
173
- title={item.description}
174
- aria-label={`${item.name} - ${item.description}`}
275
+ {additionalLinks.map(
276
+ (item: ProcessedMenuLinkDatum) => (
277
+ <li key={item.name} className="relative">
278
+ <Menu.Item
279
+ value={item.name}
280
+ className="menu-item block w-full text-left"
175
281
  >
176
- {item.name}
177
- <span className="absolute inset-0" />
178
- </a>
179
- </Menu.Item>
180
- </li>
181
- ))}
282
+ {item.renderAs === 'button' ? (
283
+ <button
284
+ type="button"
285
+ className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 hover:text-black focus:text-black focus:underline focus:outline-none"
286
+ title={item.description}
287
+ aria-label={`${item.name} - ${item.description}`}
288
+ hx-post="/api/v1/state"
289
+ hx-swap="none"
290
+ hx-vals={item.htmxVals}
291
+ >
292
+ {item.name}
293
+ <span className="absolute inset-0" />
294
+ </button>
295
+ ) : item.renderAs === 'a' ? (
296
+ <a
297
+ href={item.href}
298
+ className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 hover:text-black focus:text-black focus:underline focus:outline-none"
299
+ title={item.description}
300
+ aria-label={`${item.name} - ${item.description}`}
301
+ >
302
+ {item.name}
303
+ <span className="absolute inset-0" />
304
+ </a>
305
+ ) : (
306
+ <span
307
+ className="text-mydarkgrey block truncate rounded p-2 text-sm font-bold leading-6 opacity-50"
308
+ title={item.description}
309
+ aria-label={`${item.name} - ${item.description}`}
310
+ >
311
+ {item.name}
312
+ </span>
313
+ )}
314
+ </Menu.Item>
315
+ </li>
316
+ )
317
+ )}
182
318
  </ul>
183
319
  </div>
184
320
  )}
@@ -20,8 +20,8 @@ import type {
20
20
 
21
21
  interface BunnyVideoSetupProps {
22
22
  nodeId: string;
23
- params?: any;
24
- config?: BrandConfig;
23
+ params: any;
24
+ config: BrandConfig;
25
25
  }
26
26
 
27
27
  interface Chapter extends VideoMoment {
@@ -45,11 +45,13 @@ interface ContentMapItem {
45
45
  interface EpinetDurationSelectorProps {
46
46
  fullContentMap?: ContentMapItem[];
47
47
  isLoading?: boolean;
48
+ hourlyNodeActivity?: any;
48
49
  }
49
50
 
50
51
  const EpinetDurationSelector = ({
51
52
  fullContentMap,
52
53
  isLoading,
54
+ hourlyNodeActivity,
53
55
  }: EpinetDurationSelectorProps = {}) => {
54
56
  const [startDate, setStartDate] = useState<Date | null>(null);
55
57
  const [endDate, setEndDate] = useState<Date | null>(null);
@@ -185,22 +187,12 @@ const EpinetDurationSelector = ({
185
187
  const startUTCTime = createUTCDateTime(startDate, localFilters.startHour);
186
188
  const endUTCTime = createUTCDateTime(endDate, localFilters.endHour);
187
189
 
188
- if (endUTCTime <= startUTCTime) {
190
+ if (endUTCTime < startUTCTime) {
189
191
  setErrorMessage('End time must be after start time.');
190
192
  return;
191
193
  }
192
194
 
193
195
  const nowUTC = new Date();
194
- const maxPastTime = new Date(
195
- nowUTC.getTime() - MAX_ANALYTICS_HOURS * 60 * 60 * 1000
196
- );
197
-
198
- if (startUTCTime < maxPastTime) {
199
- setErrorMessage(
200
- `Start time cannot be more than ${MAX_ANALYTICS_HOURS} hours in the past.`
201
- );
202
- return;
203
- }
204
196
 
205
197
  if (endUTCTime > nowUTC) {
206
198
  setErrorMessage('End time cannot be in the future.');
@@ -549,7 +541,7 @@ const EpinetDurationSelector = ({
549
541
 
550
542
  return (
551
543
  <>
552
- <div className="space-y-4">
544
+ <div className="space-y-4 overflow-visible">
553
545
  {$epinetCustomFilters.enabled && (
554
546
  <div
555
547
  className={`space-y-4 rounded-lg border-2 border-dashed border-gray-200 bg-gray-50 p-4`}
@@ -865,7 +857,10 @@ const EpinetDurationSelector = ({
865
857
  </Select.Control>
866
858
  <Portal>
867
859
  <Select.Positioner>
868
- <Select.Content className="z-10 mt-2 max-h-96 w-[var(--trigger-width)] overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
860
+ <Select.Content
861
+ className="z-10 mt-2 max-h-96 overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
862
+ style={{ width: 'var(--trigger-width)' }}
863
+ >
869
864
  {paginatedUserCounts.length > 0 ? (
870
865
  [
871
866
  <Select.Item
@@ -930,6 +925,7 @@ const EpinetDurationSelector = ({
930
925
  <EpinetTableView
931
926
  fullContentMap={fullContentMap || []}
932
927
  isLoading={isLoading}
928
+ hourlyNodeActivity={hourlyNodeActivity}
933
929
  />
934
930
  )}
935
931
  </div>
@@ -53,13 +53,17 @@ interface ContentMapItem {
53
53
  type: string;
54
54
  }
55
55
 
56
+ interface Props {
57
+ fullContentMap: ContentMapItem[];
58
+ isLoading?: boolean;
59
+ hourlyNodeActivity?: any;
60
+ }
61
+
56
62
  const EpinetTableView = ({
57
63
  fullContentMap,
58
64
  isLoading = false,
59
- }: {
60
- fullContentMap: ContentMapItem[];
61
- isLoading?: boolean;
62
- }) => {
65
+ hourlyNodeActivity,
66
+ }: Props) => {
63
67
  const $epinetCustomFilters = useStore(epinetCustomFilters);
64
68
  const [currentDay, setCurrentDay] = useState<string | null>(null);
65
69
  const [availableDays, setAvailableDays] = useState<string[]>([]);
@@ -174,7 +178,7 @@ const EpinetTableView = ({
174
178
  };
175
179
 
176
180
  useEffect(() => {
177
- const hourlyActivity = $epinetCustomFilters.hourlyNodeActivity || {};
181
+ const hourlyActivity = hourlyNodeActivity || {};
178
182
  const hourKeys = Object.keys(hourlyActivity);
179
183
 
180
184
  if (hourKeys.length === 0) {
@@ -192,7 +196,7 @@ const EpinetTableView = ({
192
196
  setAvailableDays(days);
193
197
  setCurrentDay(days[0] || null);
194
198
  setCurrentDayIndex(0);
195
- }, [$epinetCustomFilters.hourlyNodeActivity]);
199
+ }, [hourlyNodeActivity]);
196
200
 
197
201
  const navigateDay = (direction: 'prev' | 'next') => {
198
202
  const newIndex =
@@ -213,7 +217,7 @@ const EpinetTableView = ({
213
217
  if (!currentDay)
214
218
  return { data: [], dailyTotal: 0, dailyVisitors: 0, maxHourlyTotal: 0 };
215
219
 
216
- const hourlyActivity = $epinetCustomFilters.hourlyNodeActivity || {};
220
+ const hourlyActivity = hourlyNodeActivity || {};
217
221
  const result: HourData[] = [];
218
222
  let emptyRangeStart: number | null = null;
219
223
  let dailyTotal = 0;
@@ -414,6 +414,7 @@ const EpinetWrapper = ({
414
414
  <EpinetDurationSelector
415
415
  fullContentMap={fullContentMap}
416
416
  isLoading={isLoading || status === 'loading'}
417
+ hourlyNodeActivity={$epinetCustomFilters.hourlyNodeActivity}
417
418
  />
418
419
  </div>
419
420
  </ErrorBoundary>
@@ -0,0 +1,105 @@
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
+ let parsedOptions;
16
+ try {
17
+ parsedOptions = JSON.parse(options?.params?.options || '{}');
18
+ } catch (e) {
19
+ console.error('Invalid options for FeaturedArticle', e);
20
+ parsedOptions = { slug: '' };
21
+ }
22
+
23
+ const slug = parsedOptions.slug || '';
24
+
25
+ const featuredStory = contentMap.find(
26
+ (item: FullContentMapItem) =>
27
+ item.slug === slug &&
28
+ item.type === 'StoryFragment' &&
29
+ item.description &&
30
+ item.panes &&
31
+ item.panes.length > 0 &&
32
+ item.thumbSrc
33
+ );
34
+ const bgColor = parsedOptions.bgColor || '';
35
+ ---
36
+
37
+ {
38
+ featuredStory ? (
39
+ <div
40
+ class="mx-auto w-full max-w-7xl px-8 py-12"
41
+ style={bgColor ? `background-color: ${bgColor}` : ''}
42
+ >
43
+ <div class="grid grid-cols-1 items-center md:grid-cols-2 md:gap-y-12">
44
+ <div class="w-full">
45
+ <a href={`/${featuredStory.slug}`} class="block">
46
+ <div class="max-w-lg pr-12">
47
+ <p class="font-action mb-4 text-lg font-bold uppercase text-gray-500">
48
+ Featured Article
49
+ </p>
50
+ <div class="space-y-6">
51
+ <h2 class="py-2 text-3xl font-bold leading-snug text-black transition-colors md:text-4xl xl:text-5xl">
52
+ {featuredStory.title}
53
+ </h2>
54
+ {featuredStory.description && (
55
+ <p class="text-sm leading-relaxed text-gray-700 md:text-lg xl:text-xl">
56
+ {featuredStory.description}
57
+ </p>
58
+ )}
59
+ {featuredStory.topics && featuredStory.topics.length > 0 && (
60
+ <div class="flex flex-wrap gap-2 pb-6 pt-2">
61
+ {featuredStory.topics.map((topic: string) => (
62
+ <span class="inline-flex items-center rounded-full bg-cyan-100 px-3 py-1 text-sm font-bold text-cyan-800">
63
+ {topic}
64
+ </span>
65
+ ))}
66
+ </div>
67
+ )}
68
+ </div>
69
+ </div>
70
+ </a>
71
+ </div>
72
+
73
+ <div class="w-full py-6">
74
+ <div class="mx-auto max-w-2xl">
75
+ <a href={`/${featuredStory.slug}`}>
76
+ <img
77
+ src={featuredStory.thumbSrc}
78
+ srcset={featuredStory.thumbSrcSet}
79
+ sizes="(min-width: 768px) 50vw, 100vw"
80
+ alt={`Preview of ${featuredStory.title}`}
81
+ class="w-full rounded-lg shadow-lg"
82
+ style="aspect-ratio: 1200 / 630;"
83
+ />
84
+ </a>
85
+ </div>
86
+ </div>
87
+ <div class="md:py-6">
88
+ <a
89
+ href={`/${featuredStory.slug}`}
90
+ 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"
91
+ >
92
+ Read More
93
+ </a>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ ) : (
98
+ <div class="mx-auto max-w-7xl px-4 py-16">
99
+ <p class="italic text-cyan-600">
100
+ Featured article not found or is missing required content (description,
101
+ panes, thumbnail).
102
+ </p>
103
+ </div>
104
+ )
105
+ }