@wealthx/shadcn 1.5.11 → 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 +88 -88
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-O5CP6VP6.mjs → chunk-BF5FKUF6.mjs} +104 -63
- package/dist/{chunk-ZMTCMP2G.mjs → chunk-EB626HVW.mjs} +70 -3
- package/dist/chunk-KICT4VQL.mjs +508 -0
- package/dist/chunk-V23CBULF.mjs +432 -0
- package/dist/components/ui/ai-conversations.js +70 -3
- package/dist/components/ui/ai-conversations.mjs +1 -1
- 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 +209 -107
- 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 +661 -403
- package/dist/index.mjs +16 -14
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/components/index.tsx +2 -0
- package/src/components/ui/ai-conversations.tsx +157 -23
- package/src/components/ui/appointment-calendar-view.tsx +211 -199
- package/src/components/ui/bank-statement-generate-dialog.tsx +147 -96
- 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,6 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { format, parseISO, subDays } from "date-fns";
|
|
3
|
-
import { cn } from "@/lib/utils";
|
|
4
3
|
import { Button } from "@/components/ui/button";
|
|
5
4
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
6
5
|
import { DatePicker } from "@/components/ui/date-picker";
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
} from "@/components/ui/dialog";
|
|
14
13
|
import { Field, FieldLabel } from "@/components/ui/field";
|
|
15
14
|
import { Input } from "@/components/ui/input";
|
|
15
|
+
import { PaginationNavButtons } from "@/components/ui/pagination";
|
|
16
16
|
import {
|
|
17
17
|
Select,
|
|
18
18
|
SelectContent,
|
|
@@ -42,7 +42,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
|
42
42
|
* - Statement name (Input)
|
|
43
43
|
* - Range preset: 90 / 180 / 365 days or Custom (ToggleGroup + DatePickers)
|
|
44
44
|
* - Applicant type: primary / secondary (Select)
|
|
45
|
-
* - Selected bank account IDs (Table with Checkboxes)
|
|
45
|
+
* - Selected bank account IDs (Table with Checkboxes, paginated at 5/page)
|
|
46
46
|
*
|
|
47
47
|
* Data handed in via props (consumer owns fetching):
|
|
48
48
|
* - `bankAccounts` — pre-filtered for the current `applicantType`
|
|
@@ -111,6 +111,17 @@ export interface BankStatementGenerateDialogProps {
|
|
|
111
111
|
className?: string;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Constants
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const APPLICANT_TYPE_LABELS: Record<"primary" | "secondary", string> = {
|
|
119
|
+
primary: "Main applicant",
|
|
120
|
+
secondary: "Co-applicant",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ACCOUNTS_PAGE_SIZE = 5;
|
|
124
|
+
|
|
114
125
|
// ---------------------------------------------------------------------------
|
|
115
126
|
// Helpers
|
|
116
127
|
// ---------------------------------------------------------------------------
|
|
@@ -144,24 +155,29 @@ export function BankStatementGenerateDialog({
|
|
|
144
155
|
}: BankStatementGenerateDialogProps) {
|
|
145
156
|
const [statementName, setStatementName] = useState("Bank Statement 1");
|
|
146
157
|
const [rangePreset, setRangePreset] = useState<BankStatementRangePreset>(90);
|
|
147
|
-
|
|
148
|
-
const [
|
|
158
|
+
// Initialise immediately so the Period column never shows "—" on first open.
|
|
159
|
+
const [fromDate, setFromDate] = useState<string>(
|
|
160
|
+
() => presetToDateRange(90).from,
|
|
161
|
+
);
|
|
162
|
+
const [toDate, setToDate] = useState<string>(() => presetToDateRange(90).to);
|
|
149
163
|
const [applicantType, setApplicantType] = useState<
|
|
150
164
|
"primary" | "secondary" | ""
|
|
151
165
|
>("");
|
|
152
166
|
const [selectedAccountIds, setSelectedAccountIds] = useState<string[]>([]);
|
|
167
|
+
const [accountPage, setAccountPage] = useState(1);
|
|
153
168
|
|
|
154
169
|
// Reset form whenever the dialog opens.
|
|
155
170
|
useEffect(() => {
|
|
156
171
|
if (!open) return;
|
|
157
172
|
const timer = setTimeout(() => {
|
|
158
|
-
setStatementName("Bank Statement 1");
|
|
159
|
-
setApplicantType("");
|
|
160
|
-
setSelectedAccountIds([]);
|
|
161
173
|
const { from, to } = presetToDateRange(90);
|
|
174
|
+
setStatementName("Bank Statement 1");
|
|
162
175
|
setRangePreset(90);
|
|
163
176
|
setFromDate(from);
|
|
164
177
|
setToDate(to);
|
|
178
|
+
setApplicantType("");
|
|
179
|
+
setSelectedAccountIds([]);
|
|
180
|
+
setAccountPage(1);
|
|
165
181
|
}, 0);
|
|
166
182
|
return () => clearTimeout(timer);
|
|
167
183
|
}, [open]);
|
|
@@ -171,12 +187,17 @@ export function BankStatementGenerateDialog({
|
|
|
171
187
|
const handleApplicantTypeChange = (type: "primary" | "secondary") => {
|
|
172
188
|
setApplicantType(type);
|
|
173
189
|
setSelectedAccountIds([]);
|
|
190
|
+
setAccountPage(1);
|
|
174
191
|
onApplicantTypeChange?.(type);
|
|
175
192
|
};
|
|
176
193
|
|
|
177
194
|
// ── Date range preset ────────────────────────────────────────────────────
|
|
178
195
|
|
|
179
|
-
const
|
|
196
|
+
const handlePresetChange = (val: string[]) => {
|
|
197
|
+
if (val.length === 0) return;
|
|
198
|
+
const raw = val[0];
|
|
199
|
+
const preset: BankStatementRangePreset =
|
|
200
|
+
raw === "custom" ? "custom" : (Number(raw) as 90 | 180 | 365);
|
|
180
201
|
setRangePreset(preset);
|
|
181
202
|
if (preset !== "custom") {
|
|
182
203
|
const { from, to } = presetToDateRange(preset);
|
|
@@ -199,13 +220,17 @@ export function BankStatementGenerateDialog({
|
|
|
199
220
|
};
|
|
200
221
|
|
|
201
222
|
const handleToggleAll = () => {
|
|
202
|
-
|
|
203
|
-
setSelectedAccountIds([]);
|
|
204
|
-
} else {
|
|
205
|
-
setSelectedAccountIds(bankAccounts.map((a) => a.id));
|
|
206
|
-
}
|
|
223
|
+
setSelectedAccountIds(areAllSelected ? [] : bankAccounts.map((a) => a.id));
|
|
207
224
|
};
|
|
208
225
|
|
|
226
|
+
// ── Accounts pagination ──────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
const accountPageCount = Math.ceil(bankAccounts.length / ACCOUNTS_PAGE_SIZE);
|
|
229
|
+
const pagedAccounts = bankAccounts.slice(
|
|
230
|
+
(accountPage - 1) * ACCOUNTS_PAGE_SIZE,
|
|
231
|
+
accountPage * ACCOUNTS_PAGE_SIZE,
|
|
232
|
+
);
|
|
233
|
+
|
|
209
234
|
// ── Period label for the accounts table ─────────────────────────────────
|
|
210
235
|
|
|
211
236
|
const periodLabel = useMemo(() => {
|
|
@@ -240,7 +265,8 @@ export function BankStatementGenerateDialog({
|
|
|
240
265
|
return (
|
|
241
266
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
242
267
|
<DialogContent
|
|
243
|
-
|
|
268
|
+
size="3xl"
|
|
269
|
+
className={className}
|
|
244
270
|
data-slot="bank-statement-generate-dialog"
|
|
245
271
|
>
|
|
246
272
|
<DialogHeader>
|
|
@@ -267,15 +293,7 @@ export function BankStatementGenerateDialog({
|
|
|
267
293
|
type="single"
|
|
268
294
|
variant="outline"
|
|
269
295
|
value={[String(rangePreset)]}
|
|
270
|
-
onValueChange={
|
|
271
|
-
if (val.length === 0) return;
|
|
272
|
-
const raw = val[0];
|
|
273
|
-
if (raw === "custom") {
|
|
274
|
-
applyPreset("custom");
|
|
275
|
-
} else {
|
|
276
|
-
applyPreset(Number(raw) as 90 | 180 | 365);
|
|
277
|
-
}
|
|
278
|
-
}}
|
|
296
|
+
onValueChange={handlePresetChange}
|
|
279
297
|
className="w-full"
|
|
280
298
|
>
|
|
281
299
|
<ToggleGroupItem value="90" className="flex-1">
|
|
@@ -317,14 +335,19 @@ export function BankStatementGenerateDialog({
|
|
|
317
335
|
onValueChange={(v) =>
|
|
318
336
|
handleApplicantTypeChange(v as "primary" | "secondary")
|
|
319
337
|
}
|
|
338
|
+
items={APPLICANT_TYPE_LABELS}
|
|
320
339
|
>
|
|
321
340
|
<SelectTrigger className="w-full">
|
|
322
341
|
<SelectValue placeholder="Applicant Type" />
|
|
323
342
|
</SelectTrigger>
|
|
324
343
|
<SelectContent>
|
|
325
|
-
<SelectItem value="primary">
|
|
344
|
+
<SelectItem value="primary">
|
|
345
|
+
{APPLICANT_TYPE_LABELS.primary}
|
|
346
|
+
</SelectItem>
|
|
326
347
|
{hasCoApplicant && (
|
|
327
|
-
<SelectItem value="secondary">
|
|
348
|
+
<SelectItem value="secondary">
|
|
349
|
+
{APPLICANT_TYPE_LABELS.secondary}
|
|
350
|
+
</SelectItem>
|
|
328
351
|
)}
|
|
329
352
|
</SelectContent>
|
|
330
353
|
</Select>
|
|
@@ -332,86 +355,114 @@ export function BankStatementGenerateDialog({
|
|
|
332
355
|
{/* Bank accounts table */}
|
|
333
356
|
{applicantType && (
|
|
334
357
|
<div className="mt-1">
|
|
335
|
-
{isLoadingAccounts
|
|
358
|
+
{isLoadingAccounts ? (
|
|
336
359
|
<div className="flex items-center justify-center p-6">
|
|
337
360
|
<Spinner size="lg" />
|
|
338
361
|
</div>
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
{!isLoadingAccounts && bankAccounts.length === 0 && (
|
|
362
|
+
) : bankAccounts.length === 0 ? (
|
|
342
363
|
<p className="py-4 text-center text-body-medium text-muted-foreground">
|
|
343
364
|
No bank accounts found for the selected applicant
|
|
344
365
|
</p>
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
<TableHead className="w-10">
|
|
352
|
-
<Checkbox
|
|
353
|
-
checked={areAllSelected}
|
|
354
|
-
indeterminate={isSomeSelected}
|
|
355
|
-
onCheckedChange={handleToggleAll}
|
|
356
|
-
aria-label="Select all accounts"
|
|
357
|
-
/>
|
|
358
|
-
</TableHead>
|
|
359
|
-
<TableHead>Account Name</TableHead>
|
|
360
|
-
<TableHead>Account Number</TableHead>
|
|
361
|
-
<TableHead>Period</TableHead>
|
|
362
|
-
<TableHead>Last Updated</TableHead>
|
|
363
|
-
</TableRow>
|
|
364
|
-
</TableHeader>
|
|
365
|
-
<TableBody>
|
|
366
|
-
{bankAccounts.map((account) => (
|
|
367
|
-
<TableRow key={account.id}>
|
|
368
|
-
<TableCell>
|
|
366
|
+
) : (
|
|
367
|
+
<>
|
|
368
|
+
<Table className="table-fixed">
|
|
369
|
+
<TableHeader>
|
|
370
|
+
<TableRow>
|
|
371
|
+
<TableHead className="w-10">
|
|
369
372
|
<Checkbox
|
|
370
|
-
checked={
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
aria-label={`Select ${account.name}`}
|
|
373
|
+
checked={areAllSelected}
|
|
374
|
+
indeterminate={isSomeSelected}
|
|
375
|
+
onCheckedChange={handleToggleAll}
|
|
376
|
+
aria-label="Select all accounts"
|
|
375
377
|
/>
|
|
376
|
-
</
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
src={account.institutionLogo}
|
|
382
|
-
alt={account.institutionName ?? ""}
|
|
383
|
-
className="size-8 rounded object-cover"
|
|
384
|
-
/>
|
|
385
|
-
)}
|
|
386
|
-
<span className="text-body-medium font-semibold">
|
|
387
|
-
{account.name || account.institutionName || "—"}
|
|
388
|
-
</span>
|
|
389
|
-
</div>
|
|
390
|
-
</TableCell>
|
|
391
|
-
<TableCell>
|
|
392
|
-
<span className="text-body-medium">
|
|
393
|
-
{account.accountNo ?? "—"}
|
|
394
|
-
</span>
|
|
395
|
-
</TableCell>
|
|
396
|
-
<TableCell>
|
|
397
|
-
<span className="text-body-medium">
|
|
398
|
-
{periodLabel}
|
|
399
|
-
</span>
|
|
400
|
-
</TableCell>
|
|
401
|
-
<TableCell>
|
|
402
|
-
<span className="text-body-medium">
|
|
403
|
-
{account.lastUpdated
|
|
404
|
-
? format(
|
|
405
|
-
parseISO(account.lastUpdated),
|
|
406
|
-
"dd MMM yyyy",
|
|
407
|
-
)
|
|
408
|
-
: "—"}
|
|
409
|
-
</span>
|
|
410
|
-
</TableCell>
|
|
378
|
+
</TableHead>
|
|
379
|
+
<TableHead>Account Name</TableHead>
|
|
380
|
+
<TableHead className="w-40">Account Number</TableHead>
|
|
381
|
+
<TableHead className="w-52">Period</TableHead>
|
|
382
|
+
<TableHead className="w-28">Last Updated</TableHead>
|
|
411
383
|
</TableRow>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
384
|
+
</TableHeader>
|
|
385
|
+
<TableBody>
|
|
386
|
+
{pagedAccounts.map((account) => {
|
|
387
|
+
const isSelected = selectedAccountIds.includes(
|
|
388
|
+
account.id,
|
|
389
|
+
);
|
|
390
|
+
return (
|
|
391
|
+
<TableRow
|
|
392
|
+
key={account.id}
|
|
393
|
+
data-state={isSelected ? "selected" : undefined}
|
|
394
|
+
>
|
|
395
|
+
<TableCell>
|
|
396
|
+
<Checkbox
|
|
397
|
+
checked={isSelected}
|
|
398
|
+
onCheckedChange={() =>
|
|
399
|
+
handleToggleAccount(account.id)
|
|
400
|
+
}
|
|
401
|
+
aria-label={`Select ${account.name}`}
|
|
402
|
+
/>
|
|
403
|
+
</TableCell>
|
|
404
|
+
<TableCell>
|
|
405
|
+
<div className="flex items-center gap-2">
|
|
406
|
+
{account.institutionLogo && (
|
|
407
|
+
<img
|
|
408
|
+
src={account.institutionLogo}
|
|
409
|
+
alt={account.institutionName ?? ""}
|
|
410
|
+
className="size-8 object-cover"
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
413
|
+
<span className="text-body-medium font-semibold">
|
|
414
|
+
{account.name ||
|
|
415
|
+
account.institutionName ||
|
|
416
|
+
"—"}
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
</TableCell>
|
|
420
|
+
<TableCell>
|
|
421
|
+
<span className="text-body-medium">
|
|
422
|
+
{account.accountNo ?? "—"}
|
|
423
|
+
</span>
|
|
424
|
+
</TableCell>
|
|
425
|
+
<TableCell>
|
|
426
|
+
<span className="text-body-medium">
|
|
427
|
+
{periodLabel}
|
|
428
|
+
</span>
|
|
429
|
+
</TableCell>
|
|
430
|
+
<TableCell>
|
|
431
|
+
<span className="text-body-medium">
|
|
432
|
+
{account.lastUpdated
|
|
433
|
+
? format(
|
|
434
|
+
parseISO(account.lastUpdated),
|
|
435
|
+
"dd MMM yyyy",
|
|
436
|
+
)
|
|
437
|
+
: "—"}
|
|
438
|
+
</span>
|
|
439
|
+
</TableCell>
|
|
440
|
+
</TableRow>
|
|
441
|
+
);
|
|
442
|
+
})}
|
|
443
|
+
</TableBody>
|
|
444
|
+
</Table>
|
|
445
|
+
{accountPageCount > 1 && (
|
|
446
|
+
<div className="flex items-center justify-between pt-2">
|
|
447
|
+
<span className="text-body-small text-muted-foreground">
|
|
448
|
+
{(accountPage - 1) * ACCOUNTS_PAGE_SIZE + 1}–
|
|
449
|
+
{Math.min(
|
|
450
|
+
accountPage * ACCOUNTS_PAGE_SIZE,
|
|
451
|
+
bankAccounts.length,
|
|
452
|
+
)}{" "}
|
|
453
|
+
of {bankAccounts.length} accounts
|
|
454
|
+
</span>
|
|
455
|
+
<PaginationNavButtons
|
|
456
|
+
hasPrev={accountPage > 1}
|
|
457
|
+
hasNext={accountPage < accountPageCount}
|
|
458
|
+
onFirst={() => setAccountPage(1)}
|
|
459
|
+
onPrev={() => setAccountPage((p) => p - 1)}
|
|
460
|
+
onNext={() => setAccountPage((p) => p + 1)}
|
|
461
|
+
onLast={() => setAccountPage(accountPageCount)}
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
</>
|
|
415
466
|
)}
|
|
416
467
|
</div>
|
|
417
468
|
)}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Center — WealthX Backoffice
|
|
3
|
+
*
|
|
4
|
+
* Full-page resource hub with video cards, email template cards, document cards,
|
|
5
|
+
* a hero header, and a generic carousel container.
|
|
6
|
+
*
|
|
7
|
+
* Component hierarchy:
|
|
8
|
+
* Molecule → ResourceVideoCard, ResourceEmailTemplateCard, ResourceDocumentCard
|
|
9
|
+
* Organism → ResourceModal, ResourceEmailEditorDialog, ResourceCarousel, ResourceCenterHeader
|
|
10
|
+
* Template → (composed by consumers)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
ResourceCarouselProps,
|
|
15
|
+
ResourceCenterHeaderProps,
|
|
16
|
+
ResourceDocumentCardProps,
|
|
17
|
+
ResourceDocumentItem,
|
|
18
|
+
ResourceEmailEditorDialogProps,
|
|
19
|
+
ResourceEmailTemplateCardProps,
|
|
20
|
+
ResourceEmailTemplateItem,
|
|
21
|
+
ResourceModalAttachment,
|
|
22
|
+
ResourceModalProps,
|
|
23
|
+
ResourceVideoCardProps,
|
|
24
|
+
ResourceVideoItem,
|
|
25
|
+
} from "./types";
|
|
26
|
+
|
|
27
|
+
export { ResourceCarousel } from "./resource-carousel";
|
|
28
|
+
export { ResourceCenterHeader } from "./resource-center-header";
|
|
29
|
+
export {
|
|
30
|
+
ResourceDocumentCard,
|
|
31
|
+
ResourceEmailTemplateCard,
|
|
32
|
+
ResourceVideoCard,
|
|
33
|
+
} from "./resource-cards";
|
|
34
|
+
export { ResourceEmailEditorDialog } from "./resource-email-editor-dialog";
|
|
35
|
+
export { ResourceModal } from "./resource-modal";
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Download, Play } from "lucide-react";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { ResourceModal } from "./resource-modal";
|
|
7
|
+
import type {
|
|
8
|
+
ResourceVideoCardProps,
|
|
9
|
+
ResourceEmailTemplateCardProps,
|
|
10
|
+
ResourceDocumentCardProps,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export function ResourceVideoCard({ video }: ResourceVideoCardProps) {
|
|
14
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() => setModalOpen(true)}
|
|
21
|
+
className="group relative flex w-full flex-col gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
22
|
+
>
|
|
23
|
+
{/* Thumbnail */}
|
|
24
|
+
<div className="relative w-full overflow-hidden bg-muted aspect-video">
|
|
25
|
+
{video.thumbnailUrl ? (
|
|
26
|
+
<img
|
|
27
|
+
src={video.thumbnailUrl}
|
|
28
|
+
alt={video.title}
|
|
29
|
+
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
|
|
30
|
+
/>
|
|
31
|
+
) : (
|
|
32
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
33
|
+
<Play className="size-10 text-muted-foreground opacity-40" />
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
{/* Play overlay on hover */}
|
|
38
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
|
39
|
+
<div className="flex size-12 items-center justify-center bg-background/90">
|
|
40
|
+
<Play className="size-5 text-foreground" />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Duration badge */}
|
|
45
|
+
{video.duration && (
|
|
46
|
+
<span className="absolute bottom-2 right-2 bg-black/70 px-1.5 py-0.5 text-caption text-white">
|
|
47
|
+
{video.duration}
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Title */}
|
|
53
|
+
<p className="text-body-medium font-semibold text-foreground leading-snug line-clamp-2">
|
|
54
|
+
{video.title}
|
|
55
|
+
</p>
|
|
56
|
+
</button>
|
|
57
|
+
|
|
58
|
+
<ResourceModal
|
|
59
|
+
open={modalOpen}
|
|
60
|
+
onClose={() => setModalOpen(false)}
|
|
61
|
+
title={video.title}
|
|
62
|
+
videoUrl={video.videoUrl}
|
|
63
|
+
tags={video.tags}
|
|
64
|
+
attachments={video.attachments}
|
|
65
|
+
/>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function ResourceEmailTemplateCard({
|
|
71
|
+
template,
|
|
72
|
+
onClick,
|
|
73
|
+
}: ResourceEmailTemplateCardProps) {
|
|
74
|
+
if (template.isAddTemplate) {
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={onClick}
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex w-full flex-col items-center justify-center gap-3",
|
|
81
|
+
"border-2 border-dashed border-border bg-muted/40",
|
|
82
|
+
"aspect-video hover:border-primary hover:bg-muted/70 transition-colors",
|
|
83
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
<div className="flex size-10 items-center justify-center border-2 border-dashed border-muted-foreground/40">
|
|
87
|
+
<span className="text-2xl text-muted-foreground/60">+</span>
|
|
88
|
+
</div>
|
|
89
|
+
<p className="text-body-small font-medium text-muted-foreground">
|
|
90
|
+
Add Template
|
|
91
|
+
</p>
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={onClick}
|
|
100
|
+
className="group relative flex w-full flex-col gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
101
|
+
>
|
|
102
|
+
{/* Preview area */}
|
|
103
|
+
<div className="relative w-full overflow-hidden border border-border bg-background aspect-video">
|
|
104
|
+
{template.htmlContent ? (
|
|
105
|
+
<iframe
|
|
106
|
+
srcDoc={template.htmlContent}
|
|
107
|
+
title={template.title}
|
|
108
|
+
className="h-full w-full pointer-events-none"
|
|
109
|
+
style={{
|
|
110
|
+
transform: "scale(0.5)",
|
|
111
|
+
transformOrigin: "top left",
|
|
112
|
+
width: "200%",
|
|
113
|
+
height: "200%",
|
|
114
|
+
}}
|
|
115
|
+
sandbox="allow-same-origin"
|
|
116
|
+
/>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="flex h-full w-full items-center justify-center bg-muted">
|
|
119
|
+
<p className="text-caption text-muted-foreground">
|
|
120
|
+
No preview available
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/* Hover overlay */}
|
|
126
|
+
<div className="absolute inset-0 flex items-center justify-center bg-foreground/60 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
|
127
|
+
<span className="text-body-small font-medium text-white">
|
|
128
|
+
Use Template
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Meta */}
|
|
134
|
+
<div className="flex items-center justify-between gap-2">
|
|
135
|
+
<p className="text-body-small font-medium text-foreground leading-snug line-clamp-1">
|
|
136
|
+
{template.title}
|
|
137
|
+
</p>
|
|
138
|
+
{template.type && (
|
|
139
|
+
<Badge variant="secondary" className="shrink-0">
|
|
140
|
+
{template.type === "system" ? "System" : "Company"}
|
|
141
|
+
</Badge>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</button>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function ResourceDocumentCard({
|
|
149
|
+
document,
|
|
150
|
+
onClick,
|
|
151
|
+
}: ResourceDocumentCardProps) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex w-full flex-col gap-2">
|
|
154
|
+
{/* PDF preview */}
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={onClick}
|
|
158
|
+
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-video"
|
|
159
|
+
>
|
|
160
|
+
{document.pdfUrl ? (
|
|
161
|
+
<iframe
|
|
162
|
+
src={document.pdfUrl}
|
|
163
|
+
title={document.title}
|
|
164
|
+
className="h-full w-full pointer-events-none"
|
|
165
|
+
/>
|
|
166
|
+
) : document.thumbnailUrl ? (
|
|
167
|
+
<img
|
|
168
|
+
src={document.thumbnailUrl}
|
|
169
|
+
alt={document.title}
|
|
170
|
+
className="h-full w-full object-cover"
|
|
171
|
+
/>
|
|
172
|
+
) : (
|
|
173
|
+
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
|
174
|
+
<div className="flex items-center justify-center bg-muted-foreground/10 p-4">
|
|
175
|
+
<Download className="size-8 text-muted-foreground opacity-50" />
|
|
176
|
+
</div>
|
|
177
|
+
<span className="text-caption text-muted-foreground">
|
|
178
|
+
{document.title}
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<div className="absolute inset-0 bg-foreground/40 opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
|
184
|
+
</button>
|
|
185
|
+
|
|
186
|
+
{/* Footer: tags + download */}
|
|
187
|
+
<div className="flex items-center justify-between gap-2">
|
|
188
|
+
<div className="flex flex-col gap-1 min-w-0">
|
|
189
|
+
<p className="text-body-small font-medium text-foreground leading-snug line-clamp-1">
|
|
190
|
+
{document.title}
|
|
191
|
+
</p>
|
|
192
|
+
{document.tags && document.tags.length > 0 && (
|
|
193
|
+
<div className="flex flex-wrap gap-1">
|
|
194
|
+
{document.tags.map((tag) => (
|
|
195
|
+
<Badge key={tag} variant="secondary">
|
|
196
|
+
{tag}
|
|
197
|
+
</Badge>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
{document.downloadUrl && (
|
|
203
|
+
<a
|
|
204
|
+
href={document.downloadUrl}
|
|
205
|
+
download
|
|
206
|
+
onClick={(e) => e.stopPropagation()}
|
|
207
|
+
className="shrink-0"
|
|
208
|
+
aria-label={`Download ${document.title}`}
|
|
209
|
+
>
|
|
210
|
+
<Button variant="outline" size="icon" className="size-8">
|
|
211
|
+
<Download className="size-4" />
|
|
212
|
+
</Button>
|
|
213
|
+
</a>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|