astro-tractstack 2.0.0-rc.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +31 -8
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -9
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { Pagination } from '@ark-ui/react/pagination';
|
|
3
|
+
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
|
4
|
+
import type { CategorizedResults, FTSResult } from '@/types/tractstack';
|
|
5
|
+
import type { FullContentMapItem } from '@/types/tractstack';
|
|
6
|
+
import {
|
|
7
|
+
getResourceUrl,
|
|
8
|
+
getResourceImage,
|
|
9
|
+
getResourceDescription,
|
|
10
|
+
} from '@/utils/customHelpers';
|
|
11
|
+
|
|
12
|
+
const VERBOSE = false;
|
|
13
|
+
|
|
14
|
+
interface SearchResultsProps {
|
|
15
|
+
results: CategorizedResults;
|
|
16
|
+
contentMap: FullContentMapItem[];
|
|
17
|
+
getTypeColor: (type: string) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResultItem {
|
|
21
|
+
id: string;
|
|
22
|
+
type: 'StoryFragment' | 'ContextPane' | 'Resource';
|
|
23
|
+
title: string;
|
|
24
|
+
slug: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
topics?: string[];
|
|
27
|
+
changed?: string;
|
|
28
|
+
thumbSrc?: string;
|
|
29
|
+
categorySlug?: string;
|
|
30
|
+
url: string;
|
|
31
|
+
imageSrc: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ITEMS_PER_PAGE = 10;
|
|
35
|
+
|
|
36
|
+
export default function SearchResults({
|
|
37
|
+
results,
|
|
38
|
+
contentMap,
|
|
39
|
+
getTypeColor,
|
|
40
|
+
}: SearchResultsProps) {
|
|
41
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
42
|
+
|
|
43
|
+
const allResultItems = useMemo(() => {
|
|
44
|
+
const items: ResultItem[] = [];
|
|
45
|
+
|
|
46
|
+
if (VERBOSE)
|
|
47
|
+
console.log('DEBUG SearchResults: Processing results', {
|
|
48
|
+
storyFragmentResults: results.storyFragmentResults.length,
|
|
49
|
+
contextPaneResults: results.contextPaneResults.length,
|
|
50
|
+
resourceResults: results.resourceResults.length,
|
|
51
|
+
contentMapSize: contentMap.length,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Process StoryFragment results
|
|
55
|
+
results.storyFragmentResults.forEach((ftsResult: FTSResult, index) => {
|
|
56
|
+
if (VERBOSE) console.log(`DEBUG StoryFragment ${index}:`, ftsResult);
|
|
57
|
+
const item = contentMap.find(
|
|
58
|
+
(item) => item.id === ftsResult.ID && item.type === 'StoryFragment'
|
|
59
|
+
);
|
|
60
|
+
if (item) {
|
|
61
|
+
if (VERBOSE)
|
|
62
|
+
console.log(
|
|
63
|
+
`DEBUG StoryFragment ${index}: Found in contentMap`,
|
|
64
|
+
item
|
|
65
|
+
);
|
|
66
|
+
items.push({
|
|
67
|
+
id: item.id,
|
|
68
|
+
type: 'StoryFragment',
|
|
69
|
+
title: item.title,
|
|
70
|
+
slug: item.slug,
|
|
71
|
+
description: item.description || undefined,
|
|
72
|
+
topics: item.topics || undefined,
|
|
73
|
+
changed: item.changed || undefined,
|
|
74
|
+
thumbSrc: item.thumbSrc || undefined,
|
|
75
|
+
url: `/${item.slug}`,
|
|
76
|
+
imageSrc: item.thumbSrc || '/static.jpg',
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
if (VERBOSE)
|
|
80
|
+
console.log(
|
|
81
|
+
`DEBUG StoryFragment ${index}: NOT found in contentMap for ID ${ftsResult.ID}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Process ContextPane results
|
|
87
|
+
results.contextPaneResults.forEach((ftsResult: FTSResult, index) => {
|
|
88
|
+
if (VERBOSE) console.log(`DEBUG ContextPane ${index}:`, ftsResult);
|
|
89
|
+
const item = contentMap.find(
|
|
90
|
+
(item) => item.id === ftsResult.ID && item.type === 'Pane'
|
|
91
|
+
);
|
|
92
|
+
if (item) {
|
|
93
|
+
if (VERBOSE)
|
|
94
|
+
console.log(`DEBUG ContextPane ${index}: Found in contentMap`, item);
|
|
95
|
+
items.push({
|
|
96
|
+
id: item.id,
|
|
97
|
+
type: 'ContextPane',
|
|
98
|
+
title: item.title,
|
|
99
|
+
slug: item.slug,
|
|
100
|
+
url: `/context/${item.slug}`,
|
|
101
|
+
imageSrc: '/static.jpg',
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
if (VERBOSE)
|
|
105
|
+
console.log(
|
|
106
|
+
`DEBUG ContextPane ${index}: NOT found in contentMap for ID ${ftsResult.ID}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Process Resource results
|
|
112
|
+
results.resourceResults.forEach((ftsResult: FTSResult, index) => {
|
|
113
|
+
if (VERBOSE) console.log(`DEBUG Resource ${index}:`, ftsResult);
|
|
114
|
+
const item = contentMap.find(
|
|
115
|
+
(item) => item.id === ftsResult.ID && item.type === 'Resource'
|
|
116
|
+
);
|
|
117
|
+
if (item) {
|
|
118
|
+
if (VERBOSE)
|
|
119
|
+
console.log(`DEBUG Resource ${index}: Found in contentMap`, item);
|
|
120
|
+
|
|
121
|
+
const resourceUrl = getResourceUrl(item.categorySlug || '', item.slug);
|
|
122
|
+
const resourceImage = getResourceImage(
|
|
123
|
+
item.id,
|
|
124
|
+
item.slug,
|
|
125
|
+
item.categorySlug || ''
|
|
126
|
+
);
|
|
127
|
+
const description = getResourceDescription(
|
|
128
|
+
item.id,
|
|
129
|
+
item.slug,
|
|
130
|
+
item.categorySlug || ''
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (VERBOSE)
|
|
134
|
+
console.log(`DEBUG Resource ${index}: Helper results`, {
|
|
135
|
+
resourceUrl,
|
|
136
|
+
resourceImage,
|
|
137
|
+
description,
|
|
138
|
+
categorySlug: item.categorySlug,
|
|
139
|
+
slug: item.slug,
|
|
140
|
+
id: item.id,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
items.push({
|
|
144
|
+
id: item.id,
|
|
145
|
+
type: 'Resource',
|
|
146
|
+
title: item.title,
|
|
147
|
+
slug: item.slug,
|
|
148
|
+
description: description || undefined,
|
|
149
|
+
categorySlug: item.categorySlug || undefined,
|
|
150
|
+
url: resourceUrl,
|
|
151
|
+
imageSrc: resourceImage,
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
if (VERBOSE)
|
|
155
|
+
console.log(
|
|
156
|
+
`DEBUG Resource ${index}: NOT found in contentMap for ID ${ftsResult.ID}`
|
|
157
|
+
);
|
|
158
|
+
if (VERBOSE)
|
|
159
|
+
console.log(
|
|
160
|
+
'DEBUG: Available resource IDs in contentMap:',
|
|
161
|
+
contentMap
|
|
162
|
+
.filter((item) => item.type === 'Resource')
|
|
163
|
+
.map((item) => ({ id: item.id, title: item.title }))
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (VERBOSE) console.log('DEBUG SearchResults: Final items', items);
|
|
169
|
+
|
|
170
|
+
// Sort by whether they have real images
|
|
171
|
+
return items.sort((a, b) => {
|
|
172
|
+
const aHasRealImage = a.imageSrc !== '/static.jpg';
|
|
173
|
+
const bHasRealImage = b.imageSrc !== '/static.jpg';
|
|
174
|
+
|
|
175
|
+
if (aHasRealImage && !bHasRealImage) return -1;
|
|
176
|
+
if (!aHasRealImage && bHasRealImage) return 1;
|
|
177
|
+
return 0;
|
|
178
|
+
});
|
|
179
|
+
}, [results, contentMap]);
|
|
180
|
+
|
|
181
|
+
const totalResults = allResultItems.length;
|
|
182
|
+
const totalPages = Math.ceil(totalResults / ITEMS_PER_PAGE);
|
|
183
|
+
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
|
184
|
+
const paginatedItems = allResultItems.slice(
|
|
185
|
+
startIndex,
|
|
186
|
+
startIndex + ITEMS_PER_PAGE
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const handlePageChange = (page: number) => {
|
|
190
|
+
setCurrentPage(page);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const getResultBadge = (type: string, categorySlug?: string) => {
|
|
194
|
+
let styleType = type;
|
|
195
|
+
let label = '';
|
|
196
|
+
switch (type) {
|
|
197
|
+
case 'StoryFragment':
|
|
198
|
+
label = 'Page';
|
|
199
|
+
break;
|
|
200
|
+
case 'ContextPane':
|
|
201
|
+
label = 'Context';
|
|
202
|
+
break;
|
|
203
|
+
case 'Resource':
|
|
204
|
+
styleType = 'COLLECTION';
|
|
205
|
+
label = categorySlug || 'Resource';
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Get the color classes but extract just the text and background colors for the inline style
|
|
212
|
+
const colorClasses = getTypeColor(styleType);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<span className={`rounded px-2 py-1 text-xs ${colorClasses}`}>
|
|
216
|
+
{label}
|
|
217
|
+
</span>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (totalResults === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className="p-6">
|
|
227
|
+
<div className="mb-6">
|
|
228
|
+
<h2 className="text-mydarkgrey text-lg font-bold">
|
|
229
|
+
{totalResults} result{totalResults !== 1 ? 's' : ''} found
|
|
230
|
+
</h2>
|
|
231
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
232
|
+
Showing {startIndex + 1}-
|
|
233
|
+
{Math.min(startIndex + ITEMS_PER_PAGE, totalResults)} of{' '}
|
|
234
|
+
{totalResults}
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="mb-8 space-y-4">
|
|
239
|
+
{paginatedItems.map((item) => (
|
|
240
|
+
<div
|
|
241
|
+
key={item.id}
|
|
242
|
+
className="rounded-lg border border-gray-200 p-4 transition-colors hover:bg-gray-100"
|
|
243
|
+
>
|
|
244
|
+
<a href={item.url} className="group block">
|
|
245
|
+
<div className="flex flex-col md:flex-row md:items-start md:gap-4">
|
|
246
|
+
{/* Mobile: Full width image without overlay badge */}
|
|
247
|
+
<div
|
|
248
|
+
className="bg-mydarkgrey relative w-full overflow-hidden rounded-lg md:hidden"
|
|
249
|
+
style={{ aspectRatio: '1200/630' }}
|
|
250
|
+
>
|
|
251
|
+
<img
|
|
252
|
+
src={item.imageSrc}
|
|
253
|
+
alt={item.title}
|
|
254
|
+
className="h-full w-full object-contain"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* Desktop: Side image without overlay badge */}
|
|
259
|
+
<div
|
|
260
|
+
className="bg-mydarkgrey relative hidden flex-shrink-0 overflow-hidden rounded-lg md:block"
|
|
261
|
+
style={{ width: '240px', height: '135px' }}
|
|
262
|
+
>
|
|
263
|
+
<img
|
|
264
|
+
src={item.imageSrc}
|
|
265
|
+
alt={item.title}
|
|
266
|
+
className="h-full w-full object-contain"
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div className="mt-3 min-w-0 flex-1 md:mt-0">
|
|
271
|
+
<div className="flex items-start justify-between gap-4">
|
|
272
|
+
<div className="flex-1">
|
|
273
|
+
<h3 className="text-mydarkgrey group-hover:text-myblue mb-2 text-lg font-bold transition-colors">
|
|
274
|
+
{item.title}
|
|
275
|
+
</h3>
|
|
276
|
+
{item.description && (
|
|
277
|
+
<p className="text-mydarkgrey mb-2 text-sm">
|
|
278
|
+
{item.description}
|
|
279
|
+
</p>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{/* Category badge and topics in same row */}
|
|
283
|
+
<div className="mb-2 flex flex-wrap gap-1">
|
|
284
|
+
{/* Always show the category badge first */}
|
|
285
|
+
{getResultBadge(item.type, item.categorySlug)}
|
|
286
|
+
|
|
287
|
+
{/* Then show topics if they exist */}
|
|
288
|
+
{item.topics && item.topics.length > 0 && (
|
|
289
|
+
<>
|
|
290
|
+
{item.topics.slice(0, 3).map((topic, idx) => (
|
|
291
|
+
<span
|
|
292
|
+
key={idx}
|
|
293
|
+
className="bg-myoffwhite text-mydarkgrey rounded px-2 py-1 text-xs"
|
|
294
|
+
>
|
|
295
|
+
{topic}
|
|
296
|
+
</span>
|
|
297
|
+
))}
|
|
298
|
+
{item.topics.length > 3 && (
|
|
299
|
+
<span className="text-mydarkgrey text-xs">
|
|
300
|
+
+{item.topics.length - 3} more
|
|
301
|
+
</span>
|
|
302
|
+
)}
|
|
303
|
+
</>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</a>
|
|
311
|
+
</div>
|
|
312
|
+
))}
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{totalPages > 1 && (
|
|
316
|
+
<div className="flex justify-center">
|
|
317
|
+
<Pagination.Root
|
|
318
|
+
count={totalResults}
|
|
319
|
+
pageSize={ITEMS_PER_PAGE}
|
|
320
|
+
page={currentPage}
|
|
321
|
+
onPageChange={(details) => handlePageChange(details.page)}
|
|
322
|
+
>
|
|
323
|
+
<Pagination.PrevTrigger className="text-mydarkgrey hover:text-myblue mr-2 flex items-center gap-1 rounded px-3 py-2 text-sm font-bold transition-colors disabled:opacity-50">
|
|
324
|
+
<ChevronLeftIcon className="h-4 w-4" />
|
|
325
|
+
Previous
|
|
326
|
+
</Pagination.PrevTrigger>
|
|
327
|
+
|
|
328
|
+
<div className="flex items-center gap-1">
|
|
329
|
+
<Pagination.Context>
|
|
330
|
+
{(pagination) =>
|
|
331
|
+
pagination.pages.map((page, index) =>
|
|
332
|
+
page.type === 'page' ? (
|
|
333
|
+
<Pagination.Item
|
|
334
|
+
key={index}
|
|
335
|
+
type="page"
|
|
336
|
+
value={page.value}
|
|
337
|
+
className={`rounded px-3 py-2 text-sm font-bold transition-colors ${
|
|
338
|
+
page.value === currentPage
|
|
339
|
+
? 'bg-myblue text-white'
|
|
340
|
+
: 'text-mydarkgrey hover:text-myblue'
|
|
341
|
+
}`}
|
|
342
|
+
>
|
|
343
|
+
{page.value}
|
|
344
|
+
</Pagination.Item>
|
|
345
|
+
) : (
|
|
346
|
+
<span
|
|
347
|
+
key={index}
|
|
348
|
+
className="text-mydarkgrey px-2 text-sm"
|
|
349
|
+
>
|
|
350
|
+
{page.type === 'ellipsis' ? '...' : ''}
|
|
351
|
+
</span>
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
</Pagination.Context>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<Pagination.NextTrigger className="text-mydarkgrey hover:text-myblue ml-2 flex items-center gap-1 rounded px-3 py-2 text-sm font-bold transition-colors disabled:opacity-50">
|
|
359
|
+
Next
|
|
360
|
+
<ChevronRightIcon className="h-4 w-4" />
|
|
361
|
+
</Pagination.NextTrigger>
|
|
362
|
+
</Pagination.Root>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
|
3
|
+
import { initSearch } from '@/utils/customHelpers';
|
|
4
|
+
import SearchModal from './SearchModal';
|
|
5
|
+
import type { FullContentMapItem } from '@/types/tractstack';
|
|
6
|
+
|
|
7
|
+
interface SearchWrapperProps {
|
|
8
|
+
contentMap: FullContentMapItem[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function SearchWrapper({ contentMap }: SearchWrapperProps) {
|
|
12
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
13
|
+
|
|
14
|
+
const handleSearchOpen = () => {
|
|
15
|
+
setIsSearchOpen(true);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const handleSearchClose = () => {
|
|
19
|
+
setIsSearchOpen(false);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
initSearch();
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<button
|
|
29
|
+
onClick={handleSearchOpen}
|
|
30
|
+
className="text-myblue/80 hover:text-myblue hover:rotate-6"
|
|
31
|
+
title="Search content"
|
|
32
|
+
aria-label="Search content"
|
|
33
|
+
>
|
|
34
|
+
<MagnifyingGlassIcon className="h-6 w-6" />
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
{isSearchOpen && (
|
|
38
|
+
<SearchModal
|
|
39
|
+
isOpen={isSearchOpen}
|
|
40
|
+
onClose={handleSearchClose}
|
|
41
|
+
contentMap={contentMap}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -131,7 +131,7 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
131
131
|
};
|
|
132
132
|
|
|
133
133
|
return (
|
|
134
|
-
<div className="space-y-8"
|
|
134
|
+
<div className="space-y-8">
|
|
135
135
|
{initialize && (
|
|
136
136
|
<div className="rounded-md border border-blue-200 bg-blue-50 p-4">
|
|
137
137
|
<div className="flex">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useMemo, Component } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, Component } from 'react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { useStore } from '@nanostores/react';
|
|
4
4
|
import { epinetCustomFilters } from '@/stores/analytics';
|
|
@@ -238,7 +238,7 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
238
238
|
)}
|
|
239
239
|
|
|
240
240
|
{/* Stats Cards Grid */}
|
|
241
|
-
<div className="mb-6 grid grid-cols-
|
|
241
|
+
<div className="mb-6 grid grid-cols-3 gap-4">
|
|
242
242
|
{stats.map((item) => {
|
|
243
243
|
const period = item.period;
|
|
244
244
|
let firstTimeValue = 0,
|
|
@@ -261,11 +261,11 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
261
261
|
return (
|
|
262
262
|
<div
|
|
263
263
|
key={item.period}
|
|
264
|
-
className="rounded-lg border border-gray-100 bg-white px-
|
|
264
|
+
className="rounded-lg border border-gray-100 bg-white px-2 py-2.5 shadow-sm transition-colors hover:border-cyan-100 md:px-4 md:py-3"
|
|
265
265
|
>
|
|
266
266
|
<dt className="text-sm font-bold text-gray-800">{item.name}</dt>
|
|
267
267
|
|
|
268
|
-
<dd className="mt-2">
|
|
268
|
+
<dd className="mt-1 md:mt-2">
|
|
269
269
|
<div className="flex items-end justify-between">
|
|
270
270
|
<div className="flex-1">
|
|
271
271
|
<div className="text-sm text-gray-600">Events</div>
|
|
@@ -276,10 +276,11 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
276
276
|
</div>
|
|
277
277
|
</dd>
|
|
278
278
|
|
|
279
|
-
<hr className="my-
|
|
279
|
+
<hr className="my-1.5 border-gray-100 md:my-3.5" />
|
|
280
280
|
|
|
281
281
|
<dd>
|
|
282
|
-
|
|
282
|
+
{/* Desktop: side-by-side layout */}
|
|
283
|
+
<div className="hidden items-end justify-between md:flex">
|
|
283
284
|
<div className="flex-1">
|
|
284
285
|
<div className="text-sm text-gray-600">
|
|
285
286
|
Anonymous Visitors
|
|
@@ -299,13 +300,35 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
299
300
|
</div>
|
|
300
301
|
</div>
|
|
301
302
|
</div>
|
|
303
|
+
|
|
304
|
+
{/* Mobile: stacked layout */}
|
|
305
|
+
<div className="md:hidden">
|
|
306
|
+
<div className="mb-1.5">
|
|
307
|
+
<div className="text-sm text-gray-600">
|
|
308
|
+
Anonymous Visitors
|
|
309
|
+
</div>
|
|
310
|
+
<div className="text-2xl font-bold tracking-tight text-cyan-700">
|
|
311
|
+
{firstTimeValue === 0
|
|
312
|
+
? '-'
|
|
313
|
+
: formatNumber(firstTimeValue)}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div>
|
|
317
|
+
<div className="text-sm text-gray-600">Known Leads</div>
|
|
318
|
+
<div className="text-2xl font-bold tracking-tight text-cyan-700">
|
|
319
|
+
{returningValue === 0
|
|
320
|
+
? '-'
|
|
321
|
+
: formatNumber(returningValue)}
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
302
325
|
</dd>
|
|
303
326
|
</div>
|
|
304
327
|
);
|
|
305
328
|
})}
|
|
306
329
|
|
|
307
330
|
{/* Total Leads Card */}
|
|
308
|
-
<div className="rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100
|
|
331
|
+
<div className="col-span-3 rounded-lg border border-gray-100 bg-white px-4 py-3 shadow-sm transition-colors hover:border-cyan-100">
|
|
309
332
|
<div className="flex items-center justify-between">
|
|
310
333
|
<dt className="text-sm font-bold text-gray-800">Total Leads</dt>
|
|
311
334
|
<div className="flex items-center gap-2">
|
|
@@ -369,7 +392,7 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
369
392
|
</div>
|
|
370
393
|
|
|
371
394
|
{/* User Journey Section */}
|
|
372
|
-
<div className="mb-6 overflow-
|
|
395
|
+
<div className="mb-6 overflow-visible">
|
|
373
396
|
<h3 className="mb-4 text-lg font-bold text-gray-900">
|
|
374
397
|
User Journey Analytics
|
|
375
398
|
</h3>
|
|
@@ -416,6 +439,7 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
416
439
|
isLoading={
|
|
417
440
|
analytics.isLoading || analytics.status === 'loading'
|
|
418
441
|
}
|
|
442
|
+
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
419
443
|
/>
|
|
420
444
|
</div>
|
|
421
445
|
</ErrorBoundary>
|
|
@@ -430,6 +454,7 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
430
454
|
isLoading={
|
|
431
455
|
analytics.isLoading || analytics.status === 'loading'
|
|
432
456
|
}
|
|
457
|
+
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
433
458
|
/>
|
|
434
459
|
</>
|
|
435
460
|
)
|
|
@@ -442,6 +467,7 @@ export default function StoryKeepDashboard_Analytics({
|
|
|
442
467
|
<EpinetDurationSelector
|
|
443
468
|
fullContentMap={fullContentMap}
|
|
444
469
|
isLoading={analytics.isLoading || analytics.status === 'loading'}
|
|
470
|
+
hourlyNodeActivity={analytics.hourlyNodeActivity}
|
|
445
471
|
/>
|
|
446
472
|
</>
|
|
447
473
|
)}
|
|
@@ -68,7 +68,7 @@ export default function StoryKeepDashboard_Branding({
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
return (
|
|
71
|
-
<div className="space-y-8"
|
|
71
|
+
<div className="space-y-8">
|
|
72
72
|
<div className="border-b border-gray-200 pb-4">
|
|
73
73
|
<h2 className="text-2xl font-bold text-gray-900">
|
|
74
74
|
Brand Configuration
|
|
@@ -64,6 +64,12 @@ const StoryKeepDashboard_Content = ({
|
|
|
64
64
|
handleContentSubtabChange(tabId as any, setActiveContentTab);
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (createMenu) {
|
|
69
|
+
setActiveContentTab('manage');
|
|
70
|
+
}
|
|
71
|
+
}, [createMenu]);
|
|
72
|
+
|
|
67
73
|
// Lightweight content summary fetch with retry logic
|
|
68
74
|
useEffect(() => {
|
|
69
75
|
let retryCount = 0;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
let MODE = 'logo'; // 'wordmark' | 'logo'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
brandConfig: {
|
|
6
|
+
LOGO?: string;
|
|
7
|
+
WORDMARK?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { brandConfig } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const getAssetPath = (
|
|
14
|
+
configPath: string | undefined,
|
|
15
|
+
fallback: string
|
|
16
|
+
): string => {
|
|
17
|
+
if (configPath && configPath !== '') {
|
|
18
|
+
return configPath;
|
|
19
|
+
}
|
|
20
|
+
return fallback;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let assetUrl;
|
|
24
|
+
if (MODE === `wordmark`)
|
|
25
|
+
assetUrl = getAssetPath(brandConfig?.WORDMARK, '/brand/wordmark.svg');
|
|
26
|
+
else assetUrl = getAssetPath(brandConfig?.LOGO, '/brand/logo.svg');
|
|
27
|
+
|
|
28
|
+
// Generate positions programmatically for triple density
|
|
29
|
+
const generatePositions = () => {
|
|
30
|
+
const positions = [];
|
|
31
|
+
const rows = 15; // More rows to extend beyond boundaries
|
|
32
|
+
const cols = 12; // More cols to extend beyond boundaries
|
|
33
|
+
|
|
34
|
+
for (let row = 0; row < rows; row++) {
|
|
35
|
+
for (let col = 0; col < cols; col++) {
|
|
36
|
+
// Skip some positions for natural spacing
|
|
37
|
+
if ((row + col) % 3 !== 0) continue;
|
|
38
|
+
|
|
39
|
+
// Allow logos to extend beyond container edges (no margins)
|
|
40
|
+
const top = (row / (rows - 1)) * 120 - 10; // Extend 10% beyond top/bottom
|
|
41
|
+
const left = (col / (cols - 1)) * 120 - 10; // Extend 10% beyond left/right
|
|
42
|
+
const rotation = -45 + Math.random() * 90;
|
|
43
|
+
|
|
44
|
+
positions.push({
|
|
45
|
+
top: `${top}%`,
|
|
46
|
+
left: `${left}%`,
|
|
47
|
+
rotation: `${rotation}deg`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return positions;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const logoPositions = generatePositions();
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
assetUrl && (
|
|
60
|
+
<div
|
|
61
|
+
class="pointer-events-none absolute mr-6 overflow-hidden rounded-2xl p-1.5 md:p-3.5"
|
|
62
|
+
style={{
|
|
63
|
+
top: '2rem',
|
|
64
|
+
left: '64rem',
|
|
65
|
+
right: '0',
|
|
66
|
+
bottom: '3.5rem',
|
|
67
|
+
'mix-blend-mode': 'multiply',
|
|
68
|
+
opacity: '0.07',
|
|
69
|
+
border: '2px dashed rgba(0, 0, 0, 1)',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{logoPositions.map((position) => (
|
|
73
|
+
<img
|
|
74
|
+
src={assetUrl}
|
|
75
|
+
style={{
|
|
76
|
+
position: 'absolute',
|
|
77
|
+
top: position.top,
|
|
78
|
+
left: position.left,
|
|
79
|
+
width: '120px',
|
|
80
|
+
height: 'auto',
|
|
81
|
+
transform: `rotate(${position.rotation})`,
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|