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
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useMemo,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
type KeyboardEvent,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import MagnifyingGlassIcon from '@heroicons/react/24/outline/MagnifyingGlassIcon';
|
|
9
|
+
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
10
|
+
import { useSearch } from '@/hooks/useSearch';
|
|
11
|
+
import type {
|
|
12
|
+
FullContentMapItem,
|
|
13
|
+
DiscoverySuggestion,
|
|
14
|
+
} from '@/types/tractstack';
|
|
15
|
+
import {
|
|
16
|
+
getResourceUrl,
|
|
17
|
+
getResourceImage,
|
|
18
|
+
getResourceDescription,
|
|
19
|
+
} from '@/utils/customHelpers';
|
|
20
|
+
|
|
21
|
+
// --- TYPES ---
|
|
22
|
+
interface SearchWidgetProps {
|
|
23
|
+
fullContentMap: FullContentMapItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ResultItem {
|
|
27
|
+
id: string;
|
|
28
|
+
url: string;
|
|
29
|
+
title: string;
|
|
30
|
+
imageSrc: string;
|
|
31
|
+
thumbSrcSet?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
topics?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ITEMS_PER_PAGE = 10;
|
|
37
|
+
|
|
38
|
+
function ResultItemCard({ item }: { item: ResultItem }) {
|
|
39
|
+
return (
|
|
40
|
+
<a href={item.url} className="group block">
|
|
41
|
+
<div className="flex items-start space-x-4 rounded-md p-2 group-hover:bg-slate-200/20">
|
|
42
|
+
<div
|
|
43
|
+
className="w-36 flex-shrink-0 rounded-md bg-gray-200 md:w-48 xl:w-72"
|
|
44
|
+
style={{ aspectRatio: '1200 / 630' }}
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
src={item.imageSrc}
|
|
48
|
+
srcSet={item.thumbSrcSet}
|
|
49
|
+
alt={item.title}
|
|
50
|
+
style={{ aspectRatio: 1200 / 630 }}
|
|
51
|
+
className="w-36 flex-shrink-0 rounded-md md:w-48 xl:w-72"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex-1">
|
|
55
|
+
<h3 className="text-lg font-bold text-black transition-colors group-hover:text-gray-900">
|
|
56
|
+
{item.title}
|
|
57
|
+
</h3>
|
|
58
|
+
{item.description && (
|
|
59
|
+
<p className="line-clamp-2 text-sm text-gray-800">
|
|
60
|
+
{item.description}
|
|
61
|
+
</p>
|
|
62
|
+
)}
|
|
63
|
+
{item.topics && item.topics.length > 0 && (
|
|
64
|
+
<div className="mt-4 flex flex-wrap gap-1">
|
|
65
|
+
{item.topics.slice(0, 3).map((topic: string) => (
|
|
66
|
+
<span
|
|
67
|
+
key={topic}
|
|
68
|
+
className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800"
|
|
69
|
+
>
|
|
70
|
+
{topic}
|
|
71
|
+
</span>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</a>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function CustomPagination({
|
|
82
|
+
currentPage,
|
|
83
|
+
totalPages,
|
|
84
|
+
onPageChange,
|
|
85
|
+
}: {
|
|
86
|
+
currentPage: number;
|
|
87
|
+
totalPages: number;
|
|
88
|
+
onPageChange: (page: number) => void;
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="mt-8 flex items-center justify-center">
|
|
92
|
+
<div className="mr-4 text-sm text-gray-700">
|
|
93
|
+
Page {currentPage} of {totalPages}
|
|
94
|
+
</div>
|
|
95
|
+
<nav className="inline-flex rounded-md shadow-sm" aria-label="Pagination">
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => onPageChange(currentPage - 1)}
|
|
98
|
+
disabled={currentPage === 1}
|
|
99
|
+
className="rounded-l-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-800 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
100
|
+
>
|
|
101
|
+
Previous
|
|
102
|
+
</button>
|
|
103
|
+
<button
|
|
104
|
+
onClick={() => onPageChange(currentPage + 1)}
|
|
105
|
+
disabled={currentPage >= totalPages}
|
|
106
|
+
className="rounded-r-md border border-cyan-600 bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
107
|
+
>
|
|
108
|
+
Next
|
|
109
|
+
</button>
|
|
110
|
+
</nav>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default function SearchWidget({ fullContentMap }: SearchWidgetProps) {
|
|
116
|
+
const [query, setQuery] = useState('');
|
|
117
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
118
|
+
const [activeSearch, setActiveSearch] = useState<DiscoverySuggestion | null>(
|
|
119
|
+
null
|
|
120
|
+
);
|
|
121
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
122
|
+
|
|
123
|
+
const {
|
|
124
|
+
suggestions,
|
|
125
|
+
isDiscovering,
|
|
126
|
+
searchResults,
|
|
127
|
+
isRetrieving,
|
|
128
|
+
retrieveError,
|
|
129
|
+
discoverTerms,
|
|
130
|
+
selectSuggestion,
|
|
131
|
+
selectExactMatch,
|
|
132
|
+
clearAll,
|
|
133
|
+
} = useSearch();
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (query.trim().length >= 3) {
|
|
137
|
+
discoverTerms(query);
|
|
138
|
+
}
|
|
139
|
+
}, [query]);
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setCurrentPage(1);
|
|
143
|
+
}, [searchResults]);
|
|
144
|
+
|
|
145
|
+
const handleSuggestionSelect = (suggestion: DiscoverySuggestion) => {
|
|
146
|
+
setActiveSearch(suggestion);
|
|
147
|
+
selectSuggestion(suggestion);
|
|
148
|
+
setQuery('');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleExactMatch = (term: string) => {
|
|
152
|
+
const searchObj = { term, type: 'EXACT' as const };
|
|
153
|
+
setActiveSearch(searchObj);
|
|
154
|
+
selectExactMatch(term);
|
|
155
|
+
setQuery('');
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const handleSuggestionClick = (suggestion: DiscoverySuggestion) => {
|
|
159
|
+
handleSuggestionSelect(suggestion);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleNewSearch = () => {
|
|
163
|
+
setActiveSearch(null);
|
|
164
|
+
clearAll();
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
168
|
+
if (e.key === 'Enter' && query.trim()) {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
if (query.trim().length < 3) return;
|
|
171
|
+
|
|
172
|
+
if (suggestions.length > 0) {
|
|
173
|
+
const exactMatch = suggestions.find(
|
|
174
|
+
(s) => s.term.toLowerCase() === query.trim().toLowerCase()
|
|
175
|
+
);
|
|
176
|
+
if (exactMatch) {
|
|
177
|
+
handleSuggestionSelect(exactMatch);
|
|
178
|
+
} else {
|
|
179
|
+
handleSuggestionSelect(suggestions[0]);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
handleExactMatch(query.trim());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getTypeColor = (type: string) => {
|
|
188
|
+
switch (type) {
|
|
189
|
+
case 'TITLE':
|
|
190
|
+
return 'bg-blue-100 text-blue-800 border-blue-200';
|
|
191
|
+
case 'EXACT':
|
|
192
|
+
return 'bg-orange-100 text-orange-800 border-orange-200';
|
|
193
|
+
case 'TOPIC':
|
|
194
|
+
return 'bg-purple-100 text-purple-800 border-purple-200';
|
|
195
|
+
case 'TEXT':
|
|
196
|
+
return 'bg-green-100 text-green-800 border-green-200';
|
|
197
|
+
default:
|
|
198
|
+
return 'bg-gray-100 text-gray-800 border-gray-200';
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const suggestionForDisplay = useMemo(() => {
|
|
203
|
+
if (query.length < 3 || suggestions.length === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const exactMatch = suggestions.find(
|
|
207
|
+
(s) => s.term.toLowerCase() === query.trim().toLowerCase()
|
|
208
|
+
);
|
|
209
|
+
return exactMatch || suggestions[0];
|
|
210
|
+
}, [suggestions, query]);
|
|
211
|
+
|
|
212
|
+
const bestCompletion = suggestionForDisplay ? suggestionForDisplay.term : '';
|
|
213
|
+
|
|
214
|
+
const showCompletion =
|
|
215
|
+
bestCompletion.toLowerCase().startsWith(query.toLowerCase()) &&
|
|
216
|
+
query.length >= 3 &&
|
|
217
|
+
bestCompletion.length > query.length;
|
|
218
|
+
|
|
219
|
+
let preservedCompletion = '';
|
|
220
|
+
if (showCompletion) {
|
|
221
|
+
const completionText = bestCompletion.slice(query.length);
|
|
222
|
+
preservedCompletion = completionText.startsWith(' ')
|
|
223
|
+
? '\u00A0' + completionText.slice(1)
|
|
224
|
+
: completionText;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const allResultItems = useMemo((): ResultItem[] => {
|
|
228
|
+
if (!searchResults) return [];
|
|
229
|
+
const items: ResultItem[] = [];
|
|
230
|
+
const combined = [
|
|
231
|
+
...searchResults.storyFragmentResults,
|
|
232
|
+
...searchResults.contextPaneResults,
|
|
233
|
+
...searchResults.resourceResults,
|
|
234
|
+
];
|
|
235
|
+
combined.forEach((ftsResult) => {
|
|
236
|
+
const item = fullContentMap.find(
|
|
237
|
+
(mapItem) => mapItem.id === ftsResult.ID
|
|
238
|
+
);
|
|
239
|
+
if (item) {
|
|
240
|
+
if (item.type === 'StoryFragment') {
|
|
241
|
+
items.push({
|
|
242
|
+
id: item.id,
|
|
243
|
+
title: item.title,
|
|
244
|
+
url: `/${item.slug}`,
|
|
245
|
+
imageSrc: item.thumbSrc || '/static.jpg',
|
|
246
|
+
thumbSrcSet: item.thumbSrcSet,
|
|
247
|
+
description: item.description,
|
|
248
|
+
topics: item.topics,
|
|
249
|
+
});
|
|
250
|
+
} else if (item.type === 'Pane' && item.isContext) {
|
|
251
|
+
items.push({
|
|
252
|
+
id: item.id,
|
|
253
|
+
title: item.title,
|
|
254
|
+
url: `/context/${item.slug}`,
|
|
255
|
+
imageSrc: '/static.jpg',
|
|
256
|
+
description: 'Contextual information page.',
|
|
257
|
+
});
|
|
258
|
+
} else if (item.type === 'Resource') {
|
|
259
|
+
items.push({
|
|
260
|
+
id: item.id,
|
|
261
|
+
title: item.title,
|
|
262
|
+
url: getResourceUrl(item.categorySlug || '', item.slug),
|
|
263
|
+
imageSrc:
|
|
264
|
+
getResourceImage(item.id, item.slug, item.categorySlug || '') ||
|
|
265
|
+
'/static.jpg',
|
|
266
|
+
description:
|
|
267
|
+
getResourceDescription(
|
|
268
|
+
item.id,
|
|
269
|
+
item.slug,
|
|
270
|
+
item.categorySlug || ''
|
|
271
|
+
) || undefined,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return items;
|
|
277
|
+
}, [searchResults, fullContentMap]);
|
|
278
|
+
|
|
279
|
+
const totalResults = allResultItems.length;
|
|
280
|
+
const totalPages = Math.ceil(totalResults / ITEMS_PER_PAGE);
|
|
281
|
+
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
|
282
|
+
const paginatedItems = allResultItems.slice(
|
|
283
|
+
startIndex,
|
|
284
|
+
startIndex + ITEMS_PER_PAGE
|
|
285
|
+
);
|
|
286
|
+
const leftColItems = paginatedItems.slice(
|
|
287
|
+
0,
|
|
288
|
+
Math.ceil(paginatedItems.length / 2)
|
|
289
|
+
);
|
|
290
|
+
const rightColItems = paginatedItems.slice(
|
|
291
|
+
Math.ceil(paginatedItems.length / 2)
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div className="mx-auto max-w-7xl px-4 py-2">
|
|
296
|
+
<div className={searchResults ? `rounded-xl border-2 p-6 md:p-12` : ``}>
|
|
297
|
+
<div
|
|
298
|
+
className={`relative mx-auto mb-8 ${!searchResults ? `max-w-5xl` : ``}`}
|
|
299
|
+
>
|
|
300
|
+
{/* PHASE 1: DISCOVERY (WHEN THERE ARE NO RESULTS) */}
|
|
301
|
+
{!searchResults && (
|
|
302
|
+
<div>
|
|
303
|
+
<div className="relative">
|
|
304
|
+
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-5">
|
|
305
|
+
<MagnifyingGlassIcon className="h-8 w-8 text-gray-400" />
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
{showCompletion && (
|
|
309
|
+
<div className="pointer-events-none absolute inset-y-0 left-0 flex h-full w-full items-center py-5 pl-16 pr-5 text-3xl text-gray-400">
|
|
310
|
+
<span
|
|
311
|
+
className="font-bold"
|
|
312
|
+
style={{ visibility: 'hidden', whiteSpace: 'pre' }}
|
|
313
|
+
>
|
|
314
|
+
{query}
|
|
315
|
+
</span>
|
|
316
|
+
<span>{preservedCompletion}</span>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
<input
|
|
321
|
+
ref={inputRef}
|
|
322
|
+
type="text"
|
|
323
|
+
value={query}
|
|
324
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
325
|
+
onKeyDown={handleKeyDown}
|
|
326
|
+
placeholder="Search for content..."
|
|
327
|
+
className="relative z-10 w-full rounded-xl border-2 border-gray-300 bg-transparent py-5 pl-16 pr-5 text-3xl font-bold text-gray-800 placeholder-gray-400 shadow-sm transition-shadow focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{isDiscovering && (
|
|
332
|
+
<div className="mx-auto mt-2 max-w-5xl rounded-xl border border-gray-200 bg-white p-4 text-center text-gray-500 shadow-lg">
|
|
333
|
+
Discovering...
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
{suggestions.length > 0 && !isDiscovering && (
|
|
337
|
+
<div className="mx-auto mt-2 max-w-5xl rounded-xl border border-gray-200 bg-white p-4 shadow-lg">
|
|
338
|
+
<p className="mb-3 text-sm font-bold text-gray-600">
|
|
339
|
+
Suggestions
|
|
340
|
+
</p>
|
|
341
|
+
<div className="flex flex-wrap gap-2">
|
|
342
|
+
{suggestions.map((suggestion, index) => (
|
|
343
|
+
<button
|
|
344
|
+
key={index}
|
|
345
|
+
onClick={() => handleSuggestionClick(suggestion)}
|
|
346
|
+
className={`inline-flex items-center rounded-lg border px-3 py-1.5 text-sm font-bold transition-all hover:shadow-md ${getTypeColor(
|
|
347
|
+
suggestion.type
|
|
348
|
+
)}`}
|
|
349
|
+
>
|
|
350
|
+
{suggestion.term}
|
|
351
|
+
</button>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
{/* Show this block only when discovery is finished, the user has typed, and there are no suggestions. */}
|
|
357
|
+
{!isDiscovering &&
|
|
358
|
+
suggestions.length === 0 &&
|
|
359
|
+
query.trim().length >= 3 && (
|
|
360
|
+
<div className="mx-auto mt-2 max-w-5xl rounded-xl border border-gray-200 bg-white p-4 text-center shadow-lg">
|
|
361
|
+
<p className="text-gray-600">
|
|
362
|
+
No suggestions found for "
|
|
363
|
+
<span className="font-bold">{query}</span>".
|
|
364
|
+
</p>
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{/* PHASE 2: RESULTS (WHEN A SEARCH HAS BEEN MADE) */}
|
|
371
|
+
{searchResults && activeSearch && (
|
|
372
|
+
<div
|
|
373
|
+
className={`inline-flex items-center gap-2 rounded-lg border-2 px-4 py-2 text-lg font-bold ${getTypeColor(activeSearch.type)}`}
|
|
374
|
+
>
|
|
375
|
+
<span>{activeSearch.term}</span>
|
|
376
|
+
<button
|
|
377
|
+
onClick={handleNewSearch}
|
|
378
|
+
className="flex items-center justify-center rounded-full text-gray-600 hover:text-gray-800"
|
|
379
|
+
aria-label={`Remove ${activeSearch.term}`}
|
|
380
|
+
>
|
|
381
|
+
<XMarkIcon className="h-5 w-5" />
|
|
382
|
+
</button>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<div className="mt-8">
|
|
388
|
+
{isRetrieving && (
|
|
389
|
+
<div className="flex justify-center p-8">
|
|
390
|
+
<div className="h-10 w-10 animate-spin rounded-full border-b-2 border-blue-600" />
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
{retrieveError && (
|
|
394
|
+
<div className="rounded-md bg-red-50 p-4 text-red-700">
|
|
395
|
+
<p>Error: {retrieveError}</p>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{searchResults && !isRetrieving && (
|
|
400
|
+
<div>
|
|
401
|
+
<div className="mb-4">
|
|
402
|
+
<h2 className="text-lg font-bold text-black">
|
|
403
|
+
{totalResults} result{totalResults !== 1 ? 's' : ''} found
|
|
404
|
+
</h2>
|
|
405
|
+
{totalResults > 0 && (
|
|
406
|
+
<p className="mt-1 text-sm text-gray-800">
|
|
407
|
+
Showing {startIndex + 1}-
|
|
408
|
+
{Math.min(startIndex + ITEMS_PER_PAGE, totalResults)} of{' '}
|
|
409
|
+
{totalResults}
|
|
410
|
+
</p>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
{totalResults > 0 ? (
|
|
414
|
+
<>
|
|
415
|
+
<div className="block space-y-6 md:hidden">
|
|
416
|
+
{paginatedItems.map((item) => (
|
|
417
|
+
<ResultItemCard key={item.id} item={item} />
|
|
418
|
+
))}
|
|
419
|
+
</div>
|
|
420
|
+
<div className="hidden md:flex md:space-x-6">
|
|
421
|
+
<div className="space-y-6 md:w-1/2">
|
|
422
|
+
{leftColItems.map((item) => (
|
|
423
|
+
<ResultItemCard key={item.id} item={item} />
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
<div className="space-y-6 md:w-1/2">
|
|
427
|
+
{rightColItems.map((item) => (
|
|
428
|
+
<ResultItemCard key={item.id} item={item} />
|
|
429
|
+
))}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
{totalPages > 1 && (
|
|
433
|
+
<CustomPagination
|
|
434
|
+
currentPage={currentPage}
|
|
435
|
+
totalPages={totalPages}
|
|
436
|
+
onPageChange={setCurrentPage}
|
|
437
|
+
/>
|
|
438
|
+
)}
|
|
439
|
+
</>
|
|
440
|
+
) : (
|
|
441
|
+
<div className="rounded-lg bg-gray-50 px-4 py-12 text-center">
|
|
442
|
+
<p className="text-lg italic text-gray-600">
|
|
443
|
+
No results found.
|
|
444
|
+
</p>
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
@@ -30,6 +30,7 @@ import StoryFragmentConfigPanel from '@/components/edit/storyfragment/StoryFragm
|
|
|
30
30
|
import StoryFragmentTitlePanel from '@/components/edit/storyfragment/StoryFragmentPanel_title';
|
|
31
31
|
import ContextPanePanel from '@/components/edit/context/ContextPaneConfig';
|
|
32
32
|
import ContextPaneTitlePanel from '@/components/edit/context/ContextPaneConfig_title';
|
|
33
|
+
import { regexpHook } from '@/constants';
|
|
33
34
|
import type {
|
|
34
35
|
StoryFragmentNode,
|
|
35
36
|
PaneNode,
|
|
@@ -40,8 +41,6 @@ import type { NodeProps } from '@/types/nodeProps';
|
|
|
40
41
|
|
|
41
42
|
function parseCodeHook(node: BaseNode | FlatNode) {
|
|
42
43
|
if ('codeHookParams' in node && Array.isArray(node.codeHookParams)) {
|
|
43
|
-
const regexpHook =
|
|
44
|
-
/^(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|signup)\((.*)\)$/;
|
|
45
44
|
const hookMatch = node.copy?.match(regexpHook);
|
|
46
45
|
|
|
47
46
|
if (!hookMatch) return null;
|
|
@@ -58,8 +57,6 @@ function parseCodeHook(node: BaseNode | FlatNode) {
|
|
|
58
57
|
const firstChild = node.children[0];
|
|
59
58
|
if (!firstChild?.value) return null;
|
|
60
59
|
|
|
61
|
-
const regexpHook =
|
|
62
|
-
/(identifyAs|youtube|bunny|bunnyContext|toggle|resource|belief|signup)\((.*?)\)/;
|
|
63
60
|
const regexpValues = /((?:[^\\|]+|\\\|?)+)/g;
|
|
64
61
|
const hookMatch = firstChild.value.match(regexpHook);
|
|
65
62
|
|
|
@@ -87,7 +84,7 @@ const getElement = (
|
|
|
87
84
|
const isPreview = getCtx(props).rootNodeId.get() === `tmp`;
|
|
88
85
|
const hasPanes = useStore(getCtx(props).hasPanes);
|
|
89
86
|
const isTemplate = useStore(getCtx(props).isTemplate);
|
|
90
|
-
const sharedProps = { nodeId: node.id, ctx: props.ctx };
|
|
87
|
+
const sharedProps = { nodeId: node.id, ctx: props.ctx, config: props.config };
|
|
91
88
|
const type = getType(node);
|
|
92
89
|
|
|
93
90
|
switch (type) {
|
|
@@ -312,7 +309,7 @@ const getElement = (
|
|
|
312
309
|
case 'impression':
|
|
313
310
|
return <></>;
|
|
314
311
|
default:
|
|
315
|
-
console.warn(`Node.tsx miss on ${type}
|
|
312
|
+
console.warn(`Node.tsx miss on ${type}`, node);
|
|
316
313
|
return <></>;
|
|
317
314
|
}
|
|
318
315
|
};
|
|
@@ -51,26 +51,36 @@ const PanelVisibilityWrapper = ({
|
|
|
51
51
|
const currentWrapper = wrapperRef.current;
|
|
52
52
|
if (!currentWrapper) return;
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// Skip intersection observer for 'add' panels - they behave differently
|
|
55
|
+
if (panelType === 'add') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
const observer = new IntersectionObserver(
|
|
56
60
|
(entries) => {
|
|
57
|
-
//
|
|
58
|
-
if (
|
|
59
|
-
|
|
61
|
+
// Add delay to prevent immediate closing during panel activation
|
|
62
|
+
if (isActive) {
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
// Double-check the panel is still active before closing
|
|
65
|
+
const currentActiveMode = nodesCtx.activePaneMode.get();
|
|
66
|
+
const stillActive =
|
|
67
|
+
currentActiveMode.panel === panelType &&
|
|
68
|
+
currentActiveMode.paneId === nodeId;
|
|
69
|
+
if (!entries[0].isIntersecting && stillActive) {
|
|
70
|
+
nodesCtx.closeAllPanels();
|
|
71
|
+
}
|
|
72
|
+
}, 100); // Small delay to allow panel to render
|
|
60
73
|
}
|
|
61
74
|
},
|
|
62
75
|
{
|
|
63
|
-
threshold: 0.1,
|
|
64
|
-
rootMargin: '-10px',
|
|
76
|
+
threshold: 0.1,
|
|
77
|
+
rootMargin: '-10px',
|
|
65
78
|
}
|
|
66
79
|
);
|
|
67
80
|
|
|
68
81
|
observer.observe(currentWrapper);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
observer.disconnect();
|
|
72
|
-
};
|
|
73
|
-
}, [nodeId, panelType, nodesCtx, isActive]); // Include isActive in dependencies
|
|
82
|
+
return () => observer.disconnect();
|
|
83
|
+
}, [nodeId, panelType, nodesCtx, isActive]);
|
|
74
84
|
|
|
75
85
|
return (
|
|
76
86
|
<div
|
|
@@ -1,49 +1,50 @@
|
|
|
1
1
|
interface BunnyVideoProps {
|
|
2
|
-
|
|
2
|
+
videoId: string;
|
|
3
3
|
title: string;
|
|
4
4
|
className?: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
// Check if a string is a valid URL
|
|
9
|
-
const isValidUrl = (url: string): boolean => {
|
|
10
|
-
try {
|
|
11
|
-
new URL(url);
|
|
12
|
-
return true;
|
|
13
|
-
} catch (e) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
7
|
+
const BUNNY_EMBED_BASE_URL = 'https://iframe.mediadelivery.net/embed/';
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
if (!
|
|
9
|
+
const isValidBunnyVideoId = (id: string): boolean => {
|
|
10
|
+
if (!id) return false;
|
|
11
|
+
const videoIdRegex = /^\d+\/[a-f0-9\-]{36}$/;
|
|
12
|
+
return videoIdRegex.test(id);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const BunnyVideo = ({ videoId, title, className = '' }: BunnyVideoProps) => {
|
|
16
|
+
// If the videoId is missing or has an invalid format, render the placeholder.
|
|
17
|
+
if (!isValidBunnyVideoId(videoId)) {
|
|
20
18
|
return (
|
|
21
19
|
<div
|
|
22
20
|
className={`flex aspect-video w-full items-center justify-center bg-gray-100 ${className}`}
|
|
23
21
|
>
|
|
24
22
|
<div className="p-4 text-center">
|
|
25
|
-
<div className="text-mydarkgrey mb-2">Video
|
|
23
|
+
<div className="text-mydarkgrey mb-2">Video ID not set</div>
|
|
26
24
|
<div className="text-mygrey text-sm">
|
|
27
|
-
Configure this widget with a valid Bunny
|
|
25
|
+
Configure this widget with a valid Bunny Video ID
|
|
28
26
|
</div>
|
|
29
27
|
</div>
|
|
30
28
|
</div>
|
|
31
29
|
);
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
// Build
|
|
35
|
-
let
|
|
32
|
+
// Build the full, final URL from the videoId.
|
|
33
|
+
let finalVideoUrl;
|
|
36
34
|
try {
|
|
37
|
-
|
|
35
|
+
const baseURL = `${BUNNY_EMBED_BASE_URL}${videoId}`;
|
|
36
|
+
const videoUrl = new URL(baseURL);
|
|
38
37
|
videoUrl.searchParams.set('autoplay', '0');
|
|
39
38
|
videoUrl.searchParams.set('preload', 'false');
|
|
40
39
|
videoUrl.searchParams.set('responsive', 'true');
|
|
40
|
+
finalVideoUrl = videoUrl.toString();
|
|
41
41
|
} catch (e) {
|
|
42
|
+
// This handles cases where a malformed videoId might be passed.
|
|
42
43
|
return (
|
|
43
44
|
<div
|
|
44
45
|
className={`flex aspect-video w-full items-center justify-center bg-gray-100 ${className}`}
|
|
45
46
|
>
|
|
46
|
-
<div className="text-mydarkgrey text-center">Invalid
|
|
47
|
+
<div className="text-mydarkgrey text-center">Invalid Video ID</div>
|
|
47
48
|
</div>
|
|
48
49
|
);
|
|
49
50
|
}
|
|
@@ -51,7 +52,7 @@ const BunnyVideo = ({ embedUrl, title, className = '' }: BunnyVideoProps) => {
|
|
|
51
52
|
return (
|
|
52
53
|
<div className={`relative w-full ${className}`}>
|
|
53
54
|
<iframe
|
|
54
|
-
src={
|
|
55
|
+
src={finalVideoUrl}
|
|
55
56
|
className="aspect-video w-full"
|
|
56
57
|
title={title}
|
|
57
58
|
allow="autoplay; fullscreen"
|