@wealthx/shadcn 1.5.12 → 1.5.13
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/.turbo/turbo-build.log +74 -74
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-CPM6P63C.mjs → chunk-BF5FKUF6.mjs} +53 -24
- package/dist/chunk-KICT4VQL.mjs +508 -0
- package/dist/chunk-V23CBULF.mjs +432 -0
- package/dist/components/ui/appointment-calendar-view.js +177 -176
- package/dist/components/ui/appointment-calendar-view.mjs +1 -1
- package/dist/components/ui/bank-statement-generate-dialog.js +163 -76
- package/dist/components/ui/bank-statement-generate-dialog.mjs +2 -1
- package/dist/components/ui/resource-center/index.js +1030 -0
- package/dist/components/ui/resource-center/index.mjs +29 -0
- package/dist/index.js +540 -364
- package/dist/index.mjs +15 -13
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/components/index.tsx +2 -0
- package/src/components/ui/appointment-calendar-view.tsx +211 -199
- package/src/components/ui/bank-statement-generate-dialog.tsx +125 -97
- package/src/components/ui/resource-center/index.tsx +35 -0
- package/src/components/ui/resource-center/resource-cards.tsx +218 -0
- package/src/components/ui/resource-center/resource-carousel.tsx +122 -0
- package/src/components/ui/resource-center/resource-center-header.tsx +95 -0
- package/src/components/ui/resource-center/resource-email-editor-dialog.tsx +131 -0
- package/src/components/ui/resource-center/resource-modal.tsx +76 -0
- package/src/components/ui/resource-center/types.ts +81 -0
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +1 -1
- package/dist/chunk-IODGRCQG.mjs +0 -438
- package/dist/chunk-XYWEGBAA.mjs +0 -348
- package/dist/components/ui/resource-center.js +0 -748
- package/dist/components/ui/resource-center.mjs +0 -24
- package/src/components/ui/resource-center.tsx +0 -539
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ResourceCarousel,
|
|
3
|
-
ResourceCenterHeader,
|
|
4
|
-
ResourceDocumentCard,
|
|
5
|
-
ResourceEmailTemplateCard,
|
|
6
|
-
ResourceModal,
|
|
7
|
-
ResourceVideoCard
|
|
8
|
-
} from "../../chunk-XYWEGBAA.mjs";
|
|
9
|
-
import "../../chunk-LI2CTS5O.mjs";
|
|
10
|
-
import "../../chunk-X6RC5UWB.mjs";
|
|
11
|
-
import "../../chunk-XYSRRDBH.mjs";
|
|
12
|
-
import "../../chunk-FRCTOAKZ.mjs";
|
|
13
|
-
import "../../chunk-NOOEKOWY.mjs";
|
|
14
|
-
import "../../chunk-R4HCRDU5.mjs";
|
|
15
|
-
import "../../chunk-AFML43VJ.mjs";
|
|
16
|
-
import "../../chunk-WNQUEZJF.mjs";
|
|
17
|
-
export {
|
|
18
|
-
ResourceCarousel,
|
|
19
|
-
ResourceCenterHeader,
|
|
20
|
-
ResourceDocumentCard,
|
|
21
|
-
ResourceEmailTemplateCard,
|
|
22
|
-
ResourceModal,
|
|
23
|
-
ResourceVideoCard
|
|
24
|
-
};
|
|
@@ -1,539 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useState } from "react";
|
|
2
|
-
import { ChevronLeft, ChevronRight, Download, Play } from "lucide-react";
|
|
3
|
-
import { cn } from "@/lib/utils";
|
|
4
|
-
import { Badge } from "@/components/ui/badge";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import {
|
|
7
|
-
Dialog,
|
|
8
|
-
DialogContent,
|
|
9
|
-
DialogHeader,
|
|
10
|
-
DialogTitle,
|
|
11
|
-
} from "@/components/ui/dialog";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Resource Center — WealthX Backoffice
|
|
15
|
-
*
|
|
16
|
-
* Full-page resource hub with video cards, email template cards, document cards,
|
|
17
|
-
* a hero header, and a generic carousel container.
|
|
18
|
-
*
|
|
19
|
-
* Component hierarchy:
|
|
20
|
-
* Atom → (no separate atoms — primitives inlined per co-located pattern)
|
|
21
|
-
* Molecule → ResourceVideoCard, ResourceEmailTemplateCard, ResourceDocumentCard
|
|
22
|
-
* Organism → ResourceModal, ResourceCarousel, ResourceCenterHeader
|
|
23
|
-
* Template → (composed by consumers)
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// ResourceModal
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
export interface ResourceModalAttachment {
|
|
31
|
-
name: string;
|
|
32
|
-
url: string;
|
|
33
|
-
type?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface ResourceModalProps {
|
|
37
|
-
open: boolean;
|
|
38
|
-
onClose: () => void;
|
|
39
|
-
title: string;
|
|
40
|
-
videoUrl?: string;
|
|
41
|
-
tags?: string[];
|
|
42
|
-
attachments?: ResourceModalAttachment[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function ResourceModal({
|
|
46
|
-
open,
|
|
47
|
-
onClose,
|
|
48
|
-
title,
|
|
49
|
-
videoUrl,
|
|
50
|
-
tags,
|
|
51
|
-
attachments,
|
|
52
|
-
}: ResourceModalProps) {
|
|
53
|
-
return (
|
|
54
|
-
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
|
55
|
-
<DialogContent
|
|
56
|
-
size="2xl"
|
|
57
|
-
className="flex flex-col gap-4 p-0 overflow-hidden"
|
|
58
|
-
>
|
|
59
|
-
<DialogHeader className="px-6 pt-6 pb-0">
|
|
60
|
-
<DialogTitle className="text-lg font-semibold">{title}</DialogTitle>
|
|
61
|
-
{tags && tags.length > 0 && (
|
|
62
|
-
<div className="flex flex-wrap gap-1.5 pt-1">
|
|
63
|
-
{tags.map((tag) => (
|
|
64
|
-
<Badge key={tag} variant="secondary" className="text-xs">
|
|
65
|
-
{tag}
|
|
66
|
-
</Badge>
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
69
|
-
)}
|
|
70
|
-
</DialogHeader>
|
|
71
|
-
|
|
72
|
-
{videoUrl && (
|
|
73
|
-
<div className="aspect-video w-full bg-foreground">
|
|
74
|
-
<iframe
|
|
75
|
-
src={videoUrl}
|
|
76
|
-
title={title}
|
|
77
|
-
className="h-full w-full"
|
|
78
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
79
|
-
allowFullScreen
|
|
80
|
-
/>
|
|
81
|
-
</div>
|
|
82
|
-
)}
|
|
83
|
-
|
|
84
|
-
{attachments && attachments.length > 0 && (
|
|
85
|
-
<div className="flex flex-col gap-2 px-6 pb-6">
|
|
86
|
-
<p className="text-sm font-medium text-foreground">Attachments</p>
|
|
87
|
-
<div className="flex flex-col gap-1">
|
|
88
|
-
{attachments.map((attachment, i) => (
|
|
89
|
-
<a
|
|
90
|
-
key={i}
|
|
91
|
-
href={attachment.url}
|
|
92
|
-
download
|
|
93
|
-
className="flex items-center gap-2 text-sm text-primary hover:underline"
|
|
94
|
-
>
|
|
95
|
-
<Download className="size-3.5 shrink-0" />
|
|
96
|
-
{attachment.name}
|
|
97
|
-
</a>
|
|
98
|
-
))}
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
{(!attachments || attachments.length === 0) && <div className="pb-2" />}
|
|
104
|
-
</DialogContent>
|
|
105
|
-
</Dialog>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
// ResourceVideoCard
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
export interface ResourceVideoItem {
|
|
114
|
-
id: string;
|
|
115
|
-
title: string;
|
|
116
|
-
thumbnailUrl?: string;
|
|
117
|
-
videoUrl?: string;
|
|
118
|
-
duration?: string;
|
|
119
|
-
tags?: string[];
|
|
120
|
-
attachments?: ResourceModalAttachment[];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface ResourceVideoCardProps {
|
|
124
|
-
video: ResourceVideoItem;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function ResourceVideoCard({ video }: ResourceVideoCardProps) {
|
|
128
|
-
const [modalOpen, setModalOpen] = useState(false);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<>
|
|
132
|
-
<button
|
|
133
|
-
type="button"
|
|
134
|
-
onClick={() => setModalOpen(true)}
|
|
135
|
-
className="group relative flex w-full flex-col gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
136
|
-
>
|
|
137
|
-
{/* Thumbnail */}
|
|
138
|
-
<div className="relative w-full overflow-hidden bg-muted aspect-video">
|
|
139
|
-
{video.thumbnailUrl ? (
|
|
140
|
-
<img
|
|
141
|
-
src={video.thumbnailUrl}
|
|
142
|
-
alt={video.title}
|
|
143
|
-
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
|
|
144
|
-
/>
|
|
145
|
-
) : (
|
|
146
|
-
<div className="flex h-full w-full items-center justify-center">
|
|
147
|
-
<Play className="size-10 text-muted-foreground opacity-40" />
|
|
148
|
-
</div>
|
|
149
|
-
)}
|
|
150
|
-
|
|
151
|
-
{/* Play overlay on hover */}
|
|
152
|
-
<div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
|
153
|
-
<div className="flex size-12 items-center justify-center bg-background/90">
|
|
154
|
-
<Play className="size-5 text-foreground" />
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
{/* Duration badge */}
|
|
159
|
-
{video.duration && (
|
|
160
|
-
<span className="absolute bottom-2 right-2 bg-black/70 px-1.5 py-0.5 text-xs font-medium text-white">
|
|
161
|
-
{video.duration}
|
|
162
|
-
</span>
|
|
163
|
-
)}
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{/* Title */}
|
|
167
|
-
<p className="text-sm font-medium text-foreground leading-snug line-clamp-2 group-hover:text-primary">
|
|
168
|
-
{video.title}
|
|
169
|
-
</p>
|
|
170
|
-
</button>
|
|
171
|
-
|
|
172
|
-
<ResourceModal
|
|
173
|
-
open={modalOpen}
|
|
174
|
-
onClose={() => setModalOpen(false)}
|
|
175
|
-
title={video.title}
|
|
176
|
-
videoUrl={video.videoUrl}
|
|
177
|
-
tags={video.tags}
|
|
178
|
-
attachments={video.attachments}
|
|
179
|
-
/>
|
|
180
|
-
</>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
// ResourceEmailTemplateCard
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
|
|
188
|
-
export interface ResourceEmailTemplateItem {
|
|
189
|
-
id: string;
|
|
190
|
-
title: string;
|
|
191
|
-
htmlContent?: string;
|
|
192
|
-
type?: "system" | "company";
|
|
193
|
-
isAddTemplate?: boolean;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export interface ResourceEmailTemplateCardProps {
|
|
197
|
-
template: ResourceEmailTemplateItem;
|
|
198
|
-
onClick?: () => void;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function ResourceEmailTemplateCard({
|
|
202
|
-
template,
|
|
203
|
-
onClick,
|
|
204
|
-
}: ResourceEmailTemplateCardProps) {
|
|
205
|
-
if (template.isAddTemplate) {
|
|
206
|
-
return (
|
|
207
|
-
<button
|
|
208
|
-
type="button"
|
|
209
|
-
onClick={onClick}
|
|
210
|
-
className={cn(
|
|
211
|
-
"flex w-full flex-col items-center justify-center gap-3",
|
|
212
|
-
"border-2 border-dashed border-border bg-muted/40",
|
|
213
|
-
"aspect-[3/4] hover:border-primary hover:bg-muted/70 transition-colors",
|
|
214
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
215
|
-
)}
|
|
216
|
-
>
|
|
217
|
-
<div className="flex size-10 items-center justify-center border-2 border-dashed border-muted-foreground/40">
|
|
218
|
-
<span className="text-2xl text-muted-foreground/60">+</span>
|
|
219
|
-
</div>
|
|
220
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
221
|
-
Add Template
|
|
222
|
-
</p>
|
|
223
|
-
</button>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return (
|
|
228
|
-
<button
|
|
229
|
-
type="button"
|
|
230
|
-
onClick={onClick}
|
|
231
|
-
className="group relative flex w-full flex-col gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
232
|
-
>
|
|
233
|
-
{/* Preview area */}
|
|
234
|
-
<div className="relative w-full overflow-hidden border border-border bg-background aspect-[3/4]">
|
|
235
|
-
{template.htmlContent ? (
|
|
236
|
-
<iframe
|
|
237
|
-
srcDoc={template.htmlContent}
|
|
238
|
-
title={template.title}
|
|
239
|
-
className="h-full w-full pointer-events-none"
|
|
240
|
-
style={{
|
|
241
|
-
transform: "scale(0.5)",
|
|
242
|
-
transformOrigin: "top left",
|
|
243
|
-
width: "200%",
|
|
244
|
-
height: "200%",
|
|
245
|
-
}}
|
|
246
|
-
sandbox="allow-same-origin"
|
|
247
|
-
/>
|
|
248
|
-
) : (
|
|
249
|
-
<div className="flex h-full w-full items-center justify-center bg-muted">
|
|
250
|
-
<p className="text-xs text-muted-foreground">
|
|
251
|
-
No preview available
|
|
252
|
-
</p>
|
|
253
|
-
</div>
|
|
254
|
-
)}
|
|
255
|
-
|
|
256
|
-
{/* Hover overlay */}
|
|
257
|
-
<div className="absolute inset-0 flex items-center justify-center bg-foreground/60 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
|
258
|
-
<span className="text-sm font-medium text-white">Use Template</span>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
{/* Meta */}
|
|
263
|
-
<div className="flex items-center justify-between gap-2">
|
|
264
|
-
<p className="text-sm font-medium text-foreground leading-snug line-clamp-1">
|
|
265
|
-
{template.title}
|
|
266
|
-
</p>
|
|
267
|
-
{template.type && (
|
|
268
|
-
<Badge variant="secondary" className="shrink-0 text-xs">
|
|
269
|
-
{template.type === "system" ? "System" : "Company"}
|
|
270
|
-
</Badge>
|
|
271
|
-
)}
|
|
272
|
-
</div>
|
|
273
|
-
</button>
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
|
-
// ResourceDocumentCard
|
|
279
|
-
// ---------------------------------------------------------------------------
|
|
280
|
-
|
|
281
|
-
export interface ResourceDocumentItem {
|
|
282
|
-
id: string;
|
|
283
|
-
title: string;
|
|
284
|
-
thumbnailUrl?: string;
|
|
285
|
-
tags?: string[];
|
|
286
|
-
downloadUrl?: string;
|
|
287
|
-
pdfUrl?: string;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export interface ResourceDocumentCardProps {
|
|
291
|
-
document: ResourceDocumentItem;
|
|
292
|
-
onClick?: () => void;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function ResourceDocumentCard({
|
|
296
|
-
document,
|
|
297
|
-
onClick,
|
|
298
|
-
}: ResourceDocumentCardProps) {
|
|
299
|
-
return (
|
|
300
|
-
<div className="flex w-full flex-col gap-2">
|
|
301
|
-
{/* PDF preview */}
|
|
302
|
-
<button
|
|
303
|
-
type="button"
|
|
304
|
-
onClick={onClick}
|
|
305
|
-
className="group relative w-full overflow-hidden border border-border bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aspect-[3/4]"
|
|
306
|
-
>
|
|
307
|
-
{document.pdfUrl ? (
|
|
308
|
-
<iframe
|
|
309
|
-
src={document.pdfUrl}
|
|
310
|
-
title={document.title}
|
|
311
|
-
className="h-full w-full pointer-events-none"
|
|
312
|
-
/>
|
|
313
|
-
) : document.thumbnailUrl ? (
|
|
314
|
-
<img
|
|
315
|
-
src={document.thumbnailUrl}
|
|
316
|
-
alt={document.title}
|
|
317
|
-
className="h-full w-full object-cover"
|
|
318
|
-
/>
|
|
319
|
-
) : (
|
|
320
|
-
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
|
321
|
-
<div className="flex items-center justify-center bg-muted-foreground/10 p-4">
|
|
322
|
-
<Download className="size-8 text-muted-foreground opacity-50" />
|
|
323
|
-
</div>
|
|
324
|
-
<span className="text-xs text-muted-foreground">
|
|
325
|
-
{document.title}
|
|
326
|
-
</span>
|
|
327
|
-
</div>
|
|
328
|
-
)}
|
|
329
|
-
|
|
330
|
-
<div className="absolute inset-0 bg-foreground/40 opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
|
331
|
-
</button>
|
|
332
|
-
|
|
333
|
-
{/* Footer: tags + download */}
|
|
334
|
-
<div className="flex items-center justify-between gap-2">
|
|
335
|
-
<div className="flex flex-col gap-1 min-w-0">
|
|
336
|
-
<p className="text-sm font-medium text-foreground leading-snug line-clamp-1">
|
|
337
|
-
{document.title}
|
|
338
|
-
</p>
|
|
339
|
-
{document.tags && document.tags.length > 0 && (
|
|
340
|
-
<div className="flex flex-wrap gap-1">
|
|
341
|
-
{document.tags.map((tag) => (
|
|
342
|
-
<Badge key={tag} variant="secondary" className="text-xs">
|
|
343
|
-
{tag}
|
|
344
|
-
</Badge>
|
|
345
|
-
))}
|
|
346
|
-
</div>
|
|
347
|
-
)}
|
|
348
|
-
</div>
|
|
349
|
-
{document.downloadUrl && (
|
|
350
|
-
<a
|
|
351
|
-
href={document.downloadUrl}
|
|
352
|
-
download
|
|
353
|
-
onClick={(e) => e.stopPropagation()}
|
|
354
|
-
className="shrink-0"
|
|
355
|
-
aria-label={`Download ${document.title}`}
|
|
356
|
-
>
|
|
357
|
-
<Button variant="outline" size="icon" className="size-8">
|
|
358
|
-
<Download className="size-4" />
|
|
359
|
-
</Button>
|
|
360
|
-
</a>
|
|
361
|
-
)}
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// ---------------------------------------------------------------------------
|
|
368
|
-
// ResourceCarousel
|
|
369
|
-
// ---------------------------------------------------------------------------
|
|
370
|
-
|
|
371
|
-
export interface ResourceCarouselProps {
|
|
372
|
-
title?: React.ReactNode;
|
|
373
|
-
items: React.ReactNode[];
|
|
374
|
-
headerAction?: React.ReactNode;
|
|
375
|
-
className?: string;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export function ResourceCarousel({
|
|
379
|
-
title,
|
|
380
|
-
items,
|
|
381
|
-
headerAction,
|
|
382
|
-
className,
|
|
383
|
-
}: ResourceCarouselProps) {
|
|
384
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
385
|
-
|
|
386
|
-
function scrollLeft() {
|
|
387
|
-
scrollRef.current?.scrollBy({ left: -300, behavior: "smooth" });
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function scrollRight() {
|
|
391
|
-
scrollRef.current?.scrollBy({ left: 300, behavior: "smooth" });
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return (
|
|
395
|
-
<div className={cn("flex flex-col gap-4", className)}>
|
|
396
|
-
{/* Header row */}
|
|
397
|
-
{(title || headerAction) && (
|
|
398
|
-
<div className="flex items-center justify-between gap-4">
|
|
399
|
-
{title && (
|
|
400
|
-
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
|
|
401
|
-
)}
|
|
402
|
-
{headerAction && <div className="shrink-0">{headerAction}</div>}
|
|
403
|
-
</div>
|
|
404
|
-
)}
|
|
405
|
-
|
|
406
|
-
{/* Carousel track with hover-reveal arrows */}
|
|
407
|
-
{items.length === 0 ? (
|
|
408
|
-
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
409
|
-
No items available
|
|
410
|
-
</p>
|
|
411
|
-
) : (
|
|
412
|
-
<div className="group relative">
|
|
413
|
-
{/* Prev arrow */}
|
|
414
|
-
<button
|
|
415
|
-
type="button"
|
|
416
|
-
onClick={scrollLeft}
|
|
417
|
-
aria-label="Scroll left"
|
|
418
|
-
className={cn(
|
|
419
|
-
"absolute left-0 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2",
|
|
420
|
-
"flex size-9 items-center justify-center",
|
|
421
|
-
"border border-border bg-background shadow-sm",
|
|
422
|
-
"opacity-0 transition-opacity duration-200 group-hover:opacity-100",
|
|
423
|
-
"hover:bg-muted focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
424
|
-
)}
|
|
425
|
-
>
|
|
426
|
-
<ChevronLeft className="size-4 text-foreground" />
|
|
427
|
-
</button>
|
|
428
|
-
|
|
429
|
-
{/* Scroll container */}
|
|
430
|
-
<div
|
|
431
|
-
ref={scrollRef}
|
|
432
|
-
className="flex gap-4 overflow-x-auto scroll-smooth pb-2"
|
|
433
|
-
style={{ scrollbarWidth: "none" }}
|
|
434
|
-
>
|
|
435
|
-
{items.map((item, i) => (
|
|
436
|
-
<div key={i} className="w-[220px] shrink-0">
|
|
437
|
-
{item}
|
|
438
|
-
</div>
|
|
439
|
-
))}
|
|
440
|
-
</div>
|
|
441
|
-
|
|
442
|
-
{/* Next arrow */}
|
|
443
|
-
<button
|
|
444
|
-
type="button"
|
|
445
|
-
onClick={scrollRight}
|
|
446
|
-
aria-label="Scroll right"
|
|
447
|
-
className={cn(
|
|
448
|
-
"absolute right-0 top-1/2 z-10 translate-x-1/2 -translate-y-1/2",
|
|
449
|
-
"flex size-9 items-center justify-center",
|
|
450
|
-
"border border-border bg-background shadow-sm",
|
|
451
|
-
"opacity-0 transition-opacity duration-200 group-hover:opacity-100",
|
|
452
|
-
"hover:bg-muted focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
453
|
-
)}
|
|
454
|
-
>
|
|
455
|
-
<ChevronRight className="size-4 text-foreground" />
|
|
456
|
-
</button>
|
|
457
|
-
</div>
|
|
458
|
-
)}
|
|
459
|
-
</div>
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// ---------------------------------------------------------------------------
|
|
464
|
-
// ResourceCenterHeader
|
|
465
|
-
// ---------------------------------------------------------------------------
|
|
466
|
-
|
|
467
|
-
export interface ResourceCenterHeaderProps {
|
|
468
|
-
title: string;
|
|
469
|
-
description: string;
|
|
470
|
-
backgroundVideoUrl?: string;
|
|
471
|
-
watchVideoUrl?: string;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export function ResourceCenterHeader({
|
|
475
|
-
title,
|
|
476
|
-
description,
|
|
477
|
-
backgroundVideoUrl,
|
|
478
|
-
watchVideoUrl,
|
|
479
|
-
}: ResourceCenterHeaderProps) {
|
|
480
|
-
const [watchModalOpen, setWatchModalOpen] = useState(false);
|
|
481
|
-
|
|
482
|
-
return (
|
|
483
|
-
<>
|
|
484
|
-
<div
|
|
485
|
-
className={cn(
|
|
486
|
-
"relative flex min-h-[400px] w-full items-center overflow-hidden bg-foreground",
|
|
487
|
-
)}
|
|
488
|
-
>
|
|
489
|
-
{/* Background video */}
|
|
490
|
-
{backgroundVideoUrl ? (
|
|
491
|
-
<video
|
|
492
|
-
className="absolute inset-0 h-full w-full object-cover opacity-60 z-[1]"
|
|
493
|
-
src={backgroundVideoUrl}
|
|
494
|
-
autoPlay
|
|
495
|
-
muted
|
|
496
|
-
loop
|
|
497
|
-
playsInline
|
|
498
|
-
/>
|
|
499
|
-
) : (
|
|
500
|
-
<div className="absolute inset-0 bg-foreground z-[1]" />
|
|
501
|
-
)}
|
|
502
|
-
|
|
503
|
-
{/* Overlay tint for legibility */}
|
|
504
|
-
<div className="absolute inset-0 bg-black/40 z-[1]" />
|
|
505
|
-
|
|
506
|
-
{/* Content */}
|
|
507
|
-
<div className="relative z-[2] flex flex-col gap-4 px-8 py-12 md:px-16 max-w-3xl">
|
|
508
|
-
<h1 className="text-3xl font-bold text-background md:text-4xl">
|
|
509
|
-
{title}
|
|
510
|
-
</h1>
|
|
511
|
-
<p className="text-base text-background/80 leading-relaxed">
|
|
512
|
-
{description}
|
|
513
|
-
</p>
|
|
514
|
-
{watchVideoUrl && (
|
|
515
|
-
<div className="pt-2">
|
|
516
|
-
<Button
|
|
517
|
-
variant="outline"
|
|
518
|
-
className="border-background text-background bg-transparent hover:bg-background/10 hover:text-background"
|
|
519
|
-
onClick={() => setWatchModalOpen(true)}
|
|
520
|
-
>
|
|
521
|
-
<Play className="mr-2 size-4" />
|
|
522
|
-
Watch Now
|
|
523
|
-
</Button>
|
|
524
|
-
</div>
|
|
525
|
-
)}
|
|
526
|
-
</div>
|
|
527
|
-
</div>
|
|
528
|
-
|
|
529
|
-
{watchVideoUrl && (
|
|
530
|
-
<ResourceModal
|
|
531
|
-
open={watchModalOpen}
|
|
532
|
-
onClose={() => setWatchModalOpen(false)}
|
|
533
|
-
title={title}
|
|
534
|
-
videoUrl={watchVideoUrl}
|
|
535
|
-
/>
|
|
536
|
-
)}
|
|
537
|
-
</>
|
|
538
|
-
);
|
|
539
|
-
}
|