astro-tractstack 2.0.0-rc.9 → 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 (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 +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_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
@@ -1,738 +0,0 @@
1
- import { useState, useRef, useEffect, type DragEvent } from 'react';
2
- import { useStore } from '@nanostores/react';
3
- import { RadioGroup } from '@ark-ui/react/radio-group';
4
- import { classNames, cloneDeep } from '@/utils/helpers';
5
- import { fullContentMapStore } from '@/stores/storykeep';
6
- import { getCtx } from '@/stores/nodes';
7
- import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
8
- import type { BrandConfig } from '@/types/tractstack';
9
- import type { PaneNode } from '@/types/compositorTypes';
10
-
11
- const radioGroupStyles = `
12
- .radio-control[data-state="unchecked"] .radio-dot {
13
- background-color: #d1d5db; /* gray-300 */
14
- }
15
- .radio-control[data-state="checked"] .radio-dot {
16
- background-color: #0891b2; /* cyan-600 */
17
- }
18
- .radio-control[data-state="checked"] {
19
- border-color: #0891b2;
20
- }
21
- .radio-item[data-state="checked"] {
22
- background-color: #ecfeff;
23
- border-color: #0891b2;
24
- }
25
- `;
26
-
27
- const sortModes = [
28
- {
29
- id: 'ordered',
30
- name: 'Preferred Order',
31
- description: 'Manually arrange pages',
32
- },
33
- { id: 'popularity', name: 'Popularity', description: 'Sort by most viewed' },
34
- { id: 'recent', name: 'Most Recent', description: 'Sort by recent updates' },
35
- ];
36
-
37
- const PER_PAGE = 20;
38
-
39
- // V2 Analytics Data Structure
40
- interface StoryfragmentAnalytics {
41
- id: string;
42
- total_actions: number;
43
- unique_visitors: number;
44
- last_24h_actions: number;
45
- last_7d_actions: number;
46
- last_28d_actions: number;
47
- last_24h_unique_visitors: number;
48
- last_7d_unique_visitors: number;
49
- last_28d_unique_visitors: number;
50
- total_leads: number;
51
- }
52
-
53
- interface FeaturedContentSetupProps {
54
- params?: Record<string, string>;
55
- nodeId: string;
56
- config?: BrandConfig;
57
- }
58
-
59
- const FeaturedContentSetup = ({
60
- params,
61
- nodeId,
62
- config,
63
- }: FeaturedContentSetupProps) => {
64
- const [isAnalyticsLoading, setIsAnalyticsLoading] = useState(true);
65
- const [analyticsData, setAnalyticsData] = useState<
66
- Record<string, StoryfragmentAnalytics>
67
- >({});
68
- const $contentMap = useStore(fullContentMapStore);
69
- const draggedRef = useRef<string | null>(null);
70
- const isInitialMount = useRef(true);
71
-
72
- const [isPanelOpen, setIsPanelOpen] = useState(false);
73
- const [selectedMode, setSelectedMode] = useState(
74
- params?.defaultMode || 'ordered'
75
- );
76
- const [selectedFeaturedId, setSelectedFeaturedId] = useState(
77
- params?.featuredId || ''
78
- );
79
- const [selectedIds, setSelectedIds] = useState<string[]>(
80
- params?.storyfragmentIds ? params.storyfragmentIds.split(',') : []
81
- );
82
- const [dragState, setDragState] = useState<{
83
- dragging: string | null;
84
- dropTarget: string | null;
85
- }>({
86
- dragging: null,
87
- dropTarget: null,
88
- });
89
- const [currentPage, setCurrentPage] = useState(1);
90
- const [bgColor, setBgColor] = useState(params?.bgColor || '');
91
-
92
- const ctx = getCtx();
93
-
94
- const hasConfiguration = selectedIds.length > 0 || selectedFeaturedId !== '';
95
-
96
- const fetchAnalyticsData = async () => {
97
- try {
98
- setIsAnalyticsLoading(true);
99
- // Updated to use V2 API endpoint
100
- const response = await fetch('/api/v1/analytics/storyfragments', {
101
- headers: {
102
- 'X-Tenant-ID': window.TRACTSTACK_CONFIG?.tenantId || 'default',
103
- },
104
- });
105
-
106
- if (!response.ok) {
107
- throw new Error(`HTTP error! status: ${response.status}`);
108
- }
109
-
110
- const analyticsArray = await response.json();
111
-
112
- // Transform array to a map keyed by ID for easier lookup
113
- // V2 API returns array directly, not wrapped in a success/data structure
114
- const analyticsById = Array.isArray(analyticsArray)
115
- ? analyticsArray.reduce(
116
- (
117
- acc: Record<string, StoryfragmentAnalytics>,
118
- item: StoryfragmentAnalytics
119
- ) => {
120
- acc[item.id] = item;
121
- return acc;
122
- },
123
- {}
124
- )
125
- : {};
126
-
127
- setAnalyticsData(analyticsById);
128
- } catch (error) {
129
- console.error('Error fetching analytics data:', error);
130
- // Set empty analytics on error to prevent blocking the UI
131
- setAnalyticsData({});
132
- } finally {
133
- setIsAnalyticsLoading(false);
134
- }
135
- };
136
-
137
- const validPages = $contentMap
138
- .filter(
139
- (item) =>
140
- item.type === 'StoryFragment' &&
141
- typeof item.description === 'string' &&
142
- typeof item.thumbSrc === 'string' &&
143
- typeof item.thumbSrcSet === 'string' &&
144
- typeof item.changed === 'string' &&
145
- item.id !== selectedFeaturedId
146
- )
147
- .sort((a, b) => {
148
- if (selectedMode === 'popularity') {
149
- return (
150
- (analyticsData[b.id]?.total_actions || 0) -
151
- (analyticsData[a.id]?.total_actions || 0)
152
- );
153
- }
154
- if (selectedMode === 'recent') {
155
- return (
156
- new Date(b.changed || 0).getTime() -
157
- new Date(a.changed || 0).getTime()
158
- );
159
- }
160
- const aIndex = selectedIds.indexOf(a.id);
161
- const bIndex = selectedIds.indexOf(b.id);
162
- return (
163
- (aIndex === -1 ? Infinity : aIndex) -
164
- (bIndex === -1 ? Infinity : bIndex)
165
- );
166
- });
167
-
168
- const featuredPage = $contentMap.find(
169
- (item) => item.id === selectedFeaturedId && item.type === 'StoryFragment'
170
- );
171
-
172
- // Build topic map
173
- const topicMap = new Map<string, { count: number; pageIds: string[] }>();
174
- validPages.forEach((page) => {
175
- if (page.topics?.length) {
176
- page.topics.forEach((topic) => {
177
- const topicData = topicMap.get(topic) || { count: 0, pageIds: [] };
178
- topicData.count += 1;
179
- topicData.pageIds.push(page.id);
180
- topicMap.set(topic, topicData);
181
- });
182
- }
183
- });
184
- const topics = Array.from(topicMap.entries()).map(
185
- ([name, { count, pageIds }]) => ({
186
- name,
187
- count,
188
- pageIds,
189
- })
190
- );
191
-
192
- const totalPages = Math.ceil(validPages.length / PER_PAGE);
193
- const paginatedPages = validPages.slice(
194
- (currentPage - 1) * PER_PAGE,
195
- currentPage * PER_PAGE
196
- );
197
-
198
- const updatePaneNode = () => {
199
- if (!nodeId) return;
200
- const allNodes = ctx.allNodes.get();
201
- const paneNode = cloneDeep(allNodes.get(nodeId)) as PaneNode;
202
- if (paneNode) {
203
- const updatedNode = {
204
- ...paneNode,
205
- codeHookTarget: 'featured-content',
206
- codeHookPayload: {
207
- options: JSON.stringify({
208
- defaultMode: selectedMode,
209
- featuredId: selectedFeaturedId,
210
- storyfragmentIds: selectedIds.join(','),
211
- bgColor,
212
- }),
213
- },
214
- bgColour: bgColor || undefined,
215
- isChanged: true,
216
- };
217
- if (!bgColor) delete updatedNode.bgColour;
218
- ctx.modifyNodes([updatedNode]);
219
- }
220
- };
221
-
222
- useEffect(() => {
223
- fetchAnalyticsData();
224
- }, []);
225
-
226
- useEffect(() => {
227
- if (isInitialMount.current) {
228
- isInitialMount.current = false;
229
- return;
230
- }
231
- const timeoutId = setTimeout(updatePaneNode, 500);
232
- return () => clearTimeout(timeoutId);
233
- }, [selectedMode, selectedFeaturedId, selectedIds, bgColor]);
234
-
235
- const moveItem = (id: string, direction: 'up' | 'down') => {
236
- const currentIndex = selectedIds.indexOf(id);
237
- if (currentIndex === -1) return;
238
- const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
239
- if (newIndex < 0 || newIndex >= selectedIds.length) return;
240
- const newOrder = [...selectedIds];
241
- [newOrder[currentIndex], newOrder[newIndex]] = [
242
- newOrder[newIndex],
243
- newOrder[currentIndex],
244
- ];
245
- setSelectedIds(newOrder);
246
- };
247
-
248
- const handleDragStart = (e: DragEvent<HTMLDivElement>, id: string) => {
249
- draggedRef.current = id;
250
- setDragState({ dragging: id, dropTarget: null });
251
- e.dataTransfer.setData('text/plain', id);
252
- };
253
-
254
- const handleDragOver = (
255
- e: DragEvent<HTMLDivElement>,
256
- id: string,
257
- isFeaturedDrop = false
258
- ) => {
259
- if (!draggedRef.current || draggedRef.current === id) return;
260
-
261
- if (isFeaturedDrop) {
262
- e.preventDefault();
263
- setDragState((prev) => ({ ...prev, dropTarget: id }));
264
- return;
265
- }
266
-
267
- if (selectedMode === 'ordered' && selectedIds.includes(id)) {
268
- e.preventDefault();
269
- setDragState((prev) => ({ ...prev, dropTarget: id }));
270
- }
271
- };
272
-
273
- const handleDrop = (
274
- e: DragEvent<HTMLDivElement>,
275
- targetId: string,
276
- isFeaturedDrop = false
277
- ) => {
278
- e.preventDefault();
279
- const draggedId = draggedRef.current;
280
- if (!draggedId) return;
281
- setDragState({ dragging: null, dropTarget: null });
282
- draggedRef.current = null;
283
-
284
- if (isFeaturedDrop) {
285
- if (!selectedIds.includes(draggedId)) {
286
- setSelectedIds((prev) => [...prev, draggedId]);
287
- }
288
- setSelectedFeaturedId(draggedId);
289
- return;
290
- }
291
-
292
- if (
293
- selectedMode !== 'ordered' ||
294
- draggedId === targetId ||
295
- !selectedIds.includes(targetId)
296
- )
297
- return;
298
- const fromIndex = selectedIds.indexOf(draggedId);
299
- const toIndex = selectedIds.indexOf(targetId);
300
- const newOrder = [...selectedIds];
301
- const [movedItem] = newOrder.splice(fromIndex, 1);
302
- newOrder.splice(toIndex, 0, movedItem);
303
- setSelectedIds(newOrder);
304
- };
305
-
306
- const toggleInclude = (id: string) => {
307
- const newSelectedIds = selectedIds.includes(id)
308
- ? selectedIds.filter((i) => i !== id)
309
- : [...selectedIds, id];
310
- setSelectedIds(newSelectedIds);
311
- if (selectedFeaturedId === id && !newSelectedIds.includes(id)) {
312
- setSelectedFeaturedId('');
313
- }
314
- };
315
-
316
- const toggleFeatured = (id: string) => {
317
- if (selectedFeaturedId === id) {
318
- setSelectedFeaturedId('');
319
- } else {
320
- if (!selectedIds.includes(id)) {
321
- setSelectedIds((prev) => [...prev, id]);
322
- }
323
- setSelectedFeaturedId(id);
324
- }
325
- };
326
-
327
- const handlePageChange = (direction: 'prev' | 'next') => {
328
- setCurrentPage((prev) =>
329
- direction === 'prev' && prev > 1
330
- ? prev - 1
331
- : direction === 'next' && prev < totalPages
332
- ? prev + 1
333
- : prev
334
- );
335
- };
336
-
337
- const handleTopicIncludeAll = (pageIds: string[]) => {
338
- const newSelectedIds = Array.from(new Set([...selectedIds, ...pageIds]));
339
- setSelectedIds(newSelectedIds);
340
- };
341
-
342
- const handleTopicExcludeAll = (topicName: string) => {
343
- const idsToExclude = selectedIds.filter((id) => {
344
- const page = $contentMap.find((p) => p.id === id);
345
- if (page?.type === 'StoryFragment') {
346
- return page.topics?.includes(topicName);
347
- }
348
- return false;
349
- });
350
-
351
- const newSelectedIds = selectedIds.filter(
352
- (id) => !idsToExclude.includes(id)
353
- );
354
- setSelectedIds(newSelectedIds);
355
-
356
- if (featuredPage && featuredPage.topics?.includes(topicName)) {
357
- setSelectedFeaturedId('');
358
- }
359
- };
360
-
361
- const getTopicIncludedCount = (pageIds: string[]) => {
362
- return pageIds.filter((id) => selectedIds.includes(id)).length;
363
- };
364
-
365
- // If panel is not open, show only the configuration button
366
- if (!isPanelOpen) {
367
- return (
368
- <div className="flex min-h-[200px] w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
369
- <button
370
- onClick={() => setIsPanelOpen(true)}
371
- className="rounded-lg bg-cyan-600 px-6 py-3 font-bold text-white shadow-md transition-colors hover:bg-cyan-700"
372
- >
373
- {hasConfiguration
374
- ? 'Edit Featured Content Widget'
375
- : 'Configure Featured Content Widget'}
376
- </button>
377
- {hasConfiguration && (
378
- <div className="mt-3 text-sm text-gray-600">
379
- Currently showing {selectedIds.length} pages
380
- {featuredPage ? ', with featured article' : ''}
381
- </div>
382
- )}
383
- </div>
384
- );
385
- }
386
-
387
- if (isAnalyticsLoading) return null;
388
- return (
389
- <div className="w-full space-y-6 bg-slate-50 p-6">
390
- <style>{radioGroupStyles}</style>
391
- <div className="flex items-center justify-between">
392
- <h2 className="text-xl font-bold text-gray-900">
393
- Configure Featured Content
394
- </h2>
395
- <button
396
- onClick={() => setIsPanelOpen(false)}
397
- className="rounded bg-gray-200 px-4 py-2 font-bold text-gray-800 transition-colors hover:bg-gray-300"
398
- >
399
- Close Configuration
400
- </button>
401
- </div>
402
-
403
- <div className="rounded-lg bg-white p-4 shadow">
404
- <h3 className="border-b border-gray-200 pb-4 text-lg font-bold text-gray-900">
405
- Settings
406
- </h3>
407
- <div className="space-y-4 pt-4">
408
- <div>
409
- <RadioGroup.Root
410
- value={selectedMode}
411
- onValueChange={(details) =>
412
- setSelectedMode(details.value || 'ordered')
413
- }
414
- >
415
- <RadioGroup.Label className="block text-sm font-bold text-gray-700">
416
- Sort Mode
417
- </RadioGroup.Label>
418
- <div className="mt-2 space-y-2">
419
- {sortModes.map((mode) => (
420
- <RadioGroup.Item
421
- key={mode.id}
422
- value={mode.id}
423
- className="radio-item flex cursor-pointer items-center rounded-md border border-gray-300 p-2"
424
- >
425
- <div className="flex items-center">
426
- <RadioGroup.ItemControl className="radio-control mr-2 flex h-4 w-4 items-center justify-center rounded-full border border-gray-300">
427
- <div className="radio-dot h-2 w-2 rounded-full" />
428
- </RadioGroup.ItemControl>
429
- <RadioGroup.ItemText>
430
- <div className="flex-1">
431
- <span className="text-sm font-bold text-gray-900">
432
- {mode.name}
433
- </span>
434
- <p className="text-xs text-gray-500">
435
- {mode.description}
436
- </p>
437
- </div>
438
- </RadioGroup.ItemText>
439
- </div>
440
- <RadioGroup.ItemHiddenInput />
441
- </RadioGroup.Item>
442
- ))}
443
- </div>
444
- </RadioGroup.Root>
445
- </div>
446
- <div>
447
- <ColorPickerCombo
448
- title="Background Color"
449
- defaultColor={bgColor}
450
- onColorChange={setBgColor}
451
- config={config!}
452
- allowNull={true}
453
- />
454
- <p className="mt-1 text-xs text-gray-500">
455
- Optional background color
456
- </p>
457
- </div>
458
- </div>
459
- </div>
460
-
461
- <div
462
- className={classNames(
463
- 'overflow-hidden rounded-lg bg-white shadow',
464
- dragState.dropTarget === 'featured'
465
- ? 'border-2 border-blue-500 bg-cyan-50'
466
- : ''
467
- )}
468
- onDragOver={(e) => handleDragOver(e, 'featured', true)}
469
- onDrop={(e) => handleDrop(e, 'featured', true)}
470
- >
471
- <div className="border-b border-gray-200 p-4">
472
- <h3 className="text-lg font-bold text-gray-900">Featured Page</h3>
473
- <p className="mt-1 text-sm text-gray-500">
474
- Drag any page here to feature it (it will be automatically included)
475
- </p>
476
- </div>
477
- {featuredPage && featuredPage.id ? (
478
- <div className="flex items-center p-4">
479
- <img
480
- src={featuredPage.thumbSrc}
481
- srcSet={featuredPage.thumbSrcSet}
482
- alt={featuredPage.title}
483
- className="h-16 w-24 flex-shrink-0 rounded object-cover"
484
- />
485
- <div className="ml-4 min-w-0 flex-1">
486
- <div className="flex items-center justify-between">
487
- <p className="truncate text-sm font-bold text-gray-900">
488
- {featuredPage.title}
489
- </p>
490
- <button
491
- onClick={() => toggleFeatured(featuredPage.id)}
492
- className="rounded bg-red-100 px-2 py-1 text-xs font-bold text-red-600 hover:bg-red-200"
493
- >
494
- Unfeature
495
- </button>
496
- </div>
497
- <p className="mt-1 line-clamp-1 text-sm text-gray-500">
498
- {featuredPage.description}
499
- </p>
500
- <div className="mt-1 text-xs text-gray-500">
501
- {analyticsData[featuredPage.id]?.total_actions || 0} views
502
- </div>
503
- </div>
504
- </div>
505
- ) : (
506
- <div className="mx-4 my-4 rounded-md border-2 border-dashed border-gray-300 p-6 text-center">
507
- <div className="flex flex-col items-center justify-center">
508
- <svg
509
- xmlns="http://www.w3.org/2000/svg"
510
- className="mb-3 h-12 w-12 text-gray-400"
511
- fill="none"
512
- viewBox="0 0 24 24"
513
- stroke="currentColor"
514
- >
515
- <path
516
- strokeLinecap="round"
517
- strokeLinejoin="round"
518
- strokeWidth={2}
519
- d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M8 10h.01M12 14h.01M16 18h.01M18 8l-6-6-6 6H6a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V10a2 2 0 00-2-2h-2l-2-2z"
520
- />
521
- </svg>
522
- <h3 className="text-sm font-bold text-gray-700">
523
- No Featured Article
524
- </h3>
525
- <p className="mt-1 text-xs text-gray-500">
526
- Select a featured article from available pages or drag a page
527
- here
528
- </p>
529
- </div>
530
- </div>
531
- )}
532
- </div>
533
-
534
- {topics.length > 0 && (
535
- <div className="overflow-hidden rounded-lg bg-white shadow">
536
- <div className="border-b border-gray-200 p-4">
537
- <h3 className="text-lg font-bold text-gray-900">Topics</h3>
538
- <p className="mt-1 text-sm text-gray-500">Manage pages by topic</p>
539
- </div>
540
- <div className="divide-y divide-gray-200">
541
- {topics.map((topic) => (
542
- <div
543
- key={topic.name}
544
- className="flex items-center justify-between p-4"
545
- >
546
- <div>
547
- <span className="text-sm font-bold text-gray-900">
548
- {topic.name}
549
- </span>
550
- <span className="ml-2 text-sm text-gray-500">
551
- ({topic.count} pages, {getTopicIncludedCount(topic.pageIds)}
552
- /{topic.count} included)
553
- </span>
554
- </div>
555
- <div className="flex gap-2">
556
- <button
557
- onClick={() => handleTopicIncludeAll(topic.pageIds)}
558
- className="rounded bg-cyan-100 px-2 py-1 text-xs font-bold text-blue-600 hover:bg-cyan-200"
559
- >
560
- Include All
561
- </button>
562
- <button
563
- onClick={() => handleTopicExcludeAll(topic.name)}
564
- className="rounded bg-red-100 px-2 py-1 text-xs font-bold text-red-600 hover:bg-red-200"
565
- >
566
- Exclude All
567
- </button>
568
- </div>
569
- </div>
570
- ))}
571
- </div>
572
- </div>
573
- )}
574
-
575
- <div className="overflow-hidden rounded-lg bg-white shadow">
576
- <div className="flex items-center justify-between border-b border-gray-200 p-4">
577
- <div>
578
- <h3 className="text-lg font-bold text-gray-900">
579
- Include additional pages
580
- </h3>
581
- <p className="mt-1 text-sm text-gray-500">
582
- Pages ({selectedIds.length - (selectedFeaturedId ? 1 : 0)}{' '}
583
- included / {validPages.length} available)
584
- </p>
585
- </div>
586
- <span className="rounded-full bg-cyan-100 px-3 py-1 text-sm font-bold text-blue-800">
587
- {selectedIds.length - (selectedFeaturedId ? 1 : 0)} /{' '}
588
- {validPages.length}
589
- </span>
590
- </div>
591
- <div className="divide-y divide-gray-200">
592
- {paginatedPages.map((page) => {
593
- const isIncluded = selectedIds.includes(page.id);
594
- const isOnly =
595
- selectedIds.includes(selectedFeaturedId) &&
596
- selectedIds.length == 2;
597
- return (
598
- <div
599
- key={page.id}
600
- draggable
601
- onDragStart={(e) => handleDragStart(e, page.id)}
602
- onDragOver={(e) => handleDragOver(e, page.id)}
603
- onDrop={(e) => handleDrop(e, page.id)}
604
- className={classNames(
605
- 'flex items-center p-4',
606
- selectedMode === 'ordered' && isIncluded ? 'cursor-move' : '',
607
- dragState.dragging === page.id
608
- ? 'bg-gray-100 opacity-50'
609
- : '',
610
- dragState.dropTarget === page.id
611
- ? 'border-2 border-blue-500 bg-cyan-50'
612
- : ''
613
- )}
614
- >
615
- <img
616
- src={page.thumbSrc}
617
- srcSet={page.thumbSrcSet}
618
- alt={page.title}
619
- className="h-16 w-24 flex-shrink-0 rounded object-cover"
620
- />
621
- <div className="ml-4 min-w-0 flex-1">
622
- <div className="flex items-center justify-between">
623
- <p className="truncate text-sm font-bold text-gray-900">
624
- {page.title}
625
- </p>
626
- <div className="flex gap-2">
627
- {selectedMode === 'ordered' && isIncluded && (
628
- <div className="flex gap-1">
629
- <button
630
- onClick={() => moveItem(page.id, 'up')}
631
- disabled={
632
- isOnly || selectedIds.indexOf(page.id) === 0
633
- }
634
- className={classNames(
635
- 'p-1',
636
- isOnly || selectedIds.indexOf(page.id) === 0
637
- ? 'cursor-not-allowed text-gray-300'
638
- : 'text-gray-500 hover:text-blue-600'
639
- )}
640
- >
641
-
642
- </button>
643
- <button
644
- onClick={() => moveItem(page.id, 'down')}
645
- disabled={
646
- isOnly ||
647
- selectedIds.indexOf(page.id) ===
648
- selectedIds.length - 1
649
- }
650
- className={classNames(
651
- 'p-1',
652
- selectedIds.indexOf(page.id) ===
653
- selectedIds.length - 1
654
- ? 'cursor-not-allowed text-gray-300'
655
- : 'text-gray-500 hover:text-blue-600'
656
- )}
657
- >
658
-
659
- </button>
660
- </div>
661
- )}
662
-
663
- <button
664
- onClick={() => toggleFeatured(page.id)}
665
- className={classNames(
666
- 'rounded px-2 py-1 text-xs font-bold',
667
- selectedFeaturedId === page.id
668
- ? 'bg-red-100 text-red-600 hover:bg-red-200'
669
- : 'bg-cyan-100 text-blue-600 hover:bg-cyan-200'
670
- )}
671
- >
672
- {selectedFeaturedId === page.id
673
- ? 'Unfeature'
674
- : 'Make Featured'}
675
- </button>
676
-
677
- <button
678
- onClick={() => toggleInclude(page.id)}
679
- className={classNames(
680
- 'rounded px-2 py-1 text-xs font-bold',
681
- isIncluded
682
- ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
683
- : 'bg-green-100 text-green-600 hover:bg-green-200'
684
- )}
685
- >
686
- {isIncluded ? 'Exclude' : 'Include'}
687
- </button>
688
- </div>
689
- </div>
690
- <p className="mt-1 line-clamp-1 text-sm text-gray-500">
691
- {page.description}
692
- </p>
693
- <div className="mt-1 text-xs text-gray-500">
694
- {analyticsData[page.id]?.total_actions || 0} views • Updated{' '}
695
- {new Date(page.changed || 0).toLocaleDateString()}
696
- </div>
697
- </div>
698
- </div>
699
- );
700
- })}
701
- </div>
702
- {totalPages > 1 && (
703
- <div className="flex justify-between p-4">
704
- <button
705
- onClick={() => handlePageChange('prev')}
706
- disabled={currentPage === 1}
707
- className={classNames(
708
- 'rounded px-4 py-2 text-sm font-bold',
709
- currentPage === 1
710
- ? 'bg-gray-200 text-gray-500'
711
- : 'bg-cyan-600 text-white hover:bg-cyan-700'
712
- )}
713
- >
714
- Previous
715
- </button>
716
- <span className="text-sm text-gray-700">
717
- Page {currentPage} of {totalPages}
718
- </span>
719
- <button
720
- onClick={() => handlePageChange('next')}
721
- disabled={currentPage === totalPages}
722
- className={classNames(
723
- 'rounded px-4 py-2 text-sm font-bold',
724
- currentPage === totalPages
725
- ? 'bg-gray-200 text-gray-500'
726
- : 'bg-cyan-600 text-white hover:bg-cyan-700'
727
- )}
728
- >
729
- Next
730
- </button>
731
- </div>
732
- )}
733
- </div>
734
- </div>
735
- );
736
- };
737
-
738
- export default FeaturedContentSetup;