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.
- package/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +31 -8
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -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;
|