@wealthx/shadcn 1.5.12 → 1.5.14
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 +79 -79
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-TC43SMIN.mjs → chunk-7N6O3VPJ.mjs} +6 -6
- package/dist/{chunk-CPM6P63C.mjs → chunk-GMF7INNS.mjs} +59 -30
- package/dist/chunk-KICT4VQL.mjs +508 -0
- package/dist/{chunk-BXL74CM2.mjs → chunk-UXWNUMQA.mjs} +4 -4
- 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-document-table.js +6 -6
- package/dist/components/ui/bank-statement-document-table.mjs +1 -1
- package/dist/components/ui/bank-statement-generate-dialog.js +164 -77
- package/dist/components/ui/bank-statement-generate-dialog.mjs +2 -1
- package/dist/components/ui/bank-statement-pdf-viewer.js +4 -4
- package/dist/components/ui/bank-statement-pdf-viewer.mjs +1 -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 +556 -380
- package/dist/index.mjs +17 -15
- 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-document-table.tsx +12 -6
- package/src/components/ui/bank-statement-generate-dialog.tsx +125 -97
- package/src/components/ui/bank-statement-pdf-viewer.tsx +4 -4
- 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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@/components/ui/dialog";
|
|
13
13
|
import { Field, FieldLabel } from "@/components/ui/field";
|
|
14
14
|
import { Input } from "@/components/ui/input";
|
|
15
|
+
import { PaginationNavButtons } from "@/components/ui/pagination";
|
|
15
16
|
import {
|
|
16
17
|
Select,
|
|
17
18
|
SelectContent,
|
|
@@ -41,7 +42,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
|
41
42
|
* - Statement name (Input)
|
|
42
43
|
* - Range preset: 90 / 180 / 365 days or Custom (ToggleGroup + DatePickers)
|
|
43
44
|
* - Applicant type: primary / secondary (Select)
|
|
44
|
-
* - Selected bank account IDs (Table with Checkboxes)
|
|
45
|
+
* - Selected bank account IDs (Table with Checkboxes, paginated at 5/page)
|
|
45
46
|
*
|
|
46
47
|
* Data handed in via props (consumer owns fetching):
|
|
47
48
|
* - `bankAccounts` — pre-filtered for the current `applicantType`
|
|
@@ -119,6 +120,8 @@ const APPLICANT_TYPE_LABELS: Record<"primary" | "secondary", string> = {
|
|
|
119
120
|
secondary: "Co-applicant",
|
|
120
121
|
};
|
|
121
122
|
|
|
123
|
+
const ACCOUNTS_PAGE_SIZE = 5;
|
|
124
|
+
|
|
122
125
|
// ---------------------------------------------------------------------------
|
|
123
126
|
// Helpers
|
|
124
127
|
// ---------------------------------------------------------------------------
|
|
@@ -161,18 +164,20 @@ export function BankStatementGenerateDialog({
|
|
|
161
164
|
"primary" | "secondary" | ""
|
|
162
165
|
>("");
|
|
163
166
|
const [selectedAccountIds, setSelectedAccountIds] = useState<string[]>([]);
|
|
167
|
+
const [accountPage, setAccountPage] = useState(1);
|
|
164
168
|
|
|
165
169
|
// Reset form whenever the dialog opens.
|
|
166
170
|
useEffect(() => {
|
|
167
171
|
if (!open) return;
|
|
168
172
|
const timer = setTimeout(() => {
|
|
169
|
-
setStatementName("Bank Statement 1");
|
|
170
|
-
setApplicantType("");
|
|
171
|
-
setSelectedAccountIds([]);
|
|
172
173
|
const { from, to } = presetToDateRange(90);
|
|
174
|
+
setStatementName("Bank Statement 1");
|
|
173
175
|
setRangePreset(90);
|
|
174
176
|
setFromDate(from);
|
|
175
177
|
setToDate(to);
|
|
178
|
+
setApplicantType("");
|
|
179
|
+
setSelectedAccountIds([]);
|
|
180
|
+
setAccountPage(1);
|
|
176
181
|
}, 0);
|
|
177
182
|
return () => clearTimeout(timer);
|
|
178
183
|
}, [open]);
|
|
@@ -182,12 +187,17 @@ export function BankStatementGenerateDialog({
|
|
|
182
187
|
const handleApplicantTypeChange = (type: "primary" | "secondary") => {
|
|
183
188
|
setApplicantType(type);
|
|
184
189
|
setSelectedAccountIds([]);
|
|
190
|
+
setAccountPage(1);
|
|
185
191
|
onApplicantTypeChange?.(type);
|
|
186
192
|
};
|
|
187
193
|
|
|
188
194
|
// ── Date range preset ────────────────────────────────────────────────────
|
|
189
195
|
|
|
190
|
-
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);
|
|
191
201
|
setRangePreset(preset);
|
|
192
202
|
if (preset !== "custom") {
|
|
193
203
|
const { from, to } = presetToDateRange(preset);
|
|
@@ -196,12 +206,6 @@ export function BankStatementGenerateDialog({
|
|
|
196
206
|
}
|
|
197
207
|
};
|
|
198
208
|
|
|
199
|
-
const handlePresetChange = (val: string[]) => {
|
|
200
|
-
if (val.length === 0) return;
|
|
201
|
-
const raw = val[0];
|
|
202
|
-
applyPreset(raw === "custom" ? "custom" : (Number(raw) as 90 | 180 | 365));
|
|
203
|
-
};
|
|
204
|
-
|
|
205
209
|
// ── Account selection ────────────────────────────────────────────────────
|
|
206
210
|
|
|
207
211
|
const areAllSelected =
|
|
@@ -216,13 +220,17 @@ export function BankStatementGenerateDialog({
|
|
|
216
220
|
};
|
|
217
221
|
|
|
218
222
|
const handleToggleAll = () => {
|
|
219
|
-
|
|
220
|
-
setSelectedAccountIds([]);
|
|
221
|
-
} else {
|
|
222
|
-
setSelectedAccountIds(bankAccounts.map((a) => a.id));
|
|
223
|
-
}
|
|
223
|
+
setSelectedAccountIds(areAllSelected ? [] : bankAccounts.map((a) => a.id));
|
|
224
224
|
};
|
|
225
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
|
+
|
|
226
234
|
// ── Period label for the accounts table ─────────────────────────────────
|
|
227
235
|
|
|
228
236
|
const periodLabel = useMemo(() => {
|
|
@@ -347,94 +355,114 @@ export function BankStatementGenerateDialog({
|
|
|
347
355
|
{/* Bank accounts table */}
|
|
348
356
|
{applicantType && (
|
|
349
357
|
<div className="mt-1">
|
|
350
|
-
{isLoadingAccounts
|
|
358
|
+
{isLoadingAccounts ? (
|
|
351
359
|
<div className="flex items-center justify-center p-6">
|
|
352
360
|
<Spinner size="lg" />
|
|
353
361
|
</div>
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
{!isLoadingAccounts && bankAccounts.length === 0 && (
|
|
362
|
+
) : bankAccounts.length === 0 ? (
|
|
357
363
|
<p className="py-4 text-center text-body-medium text-muted-foreground">
|
|
358
364
|
No bank accounts found for the selected applicant
|
|
359
365
|
</p>
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
</
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
366
|
+
) : (
|
|
367
|
+
<>
|
|
368
|
+
<Table className="table-fixed">
|
|
369
|
+
<TableHeader>
|
|
370
|
+
<TableRow>
|
|
371
|
+
<TableHead className="w-10">
|
|
372
|
+
<Checkbox
|
|
373
|
+
checked={areAllSelected}
|
|
374
|
+
indeterminate={isSomeSelected}
|
|
375
|
+
onCheckedChange={handleToggleAll}
|
|
376
|
+
aria-label="Select all accounts"
|
|
377
|
+
/>
|
|
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>
|
|
383
|
+
</TableRow>
|
|
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 min-w-0 items-center gap-2">
|
|
406
|
+
{account.institutionLogo && (
|
|
407
|
+
<img
|
|
408
|
+
src={account.institutionLogo}
|
|
409
|
+
alt={account.institutionName ?? ""}
|
|
410
|
+
className="size-8 shrink-0 object-cover"
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
413
|
+
<span className="truncate text-body-medium font-semibold">
|
|
414
|
+
{account.name ||
|
|
415
|
+
account.institutionName ||
|
|
416
|
+
"—"}
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
</TableCell>
|
|
420
|
+
<TableCell>
|
|
421
|
+
<span className="truncate text-body-medium">
|
|
422
|
+
{account.accountNo ?? "—"}
|
|
423
|
+
</span>
|
|
424
|
+
</TableCell>
|
|
425
|
+
<TableCell>
|
|
426
|
+
<span className="truncate text-body-medium">
|
|
427
|
+
{periodLabel}
|
|
428
|
+
</span>
|
|
429
|
+
</TableCell>
|
|
430
|
+
<TableCell>
|
|
431
|
+
<span className="truncate text-body-medium">
|
|
432
|
+
{account.lastUpdated
|
|
433
|
+
? format(
|
|
434
|
+
parseISO(account.lastUpdated),
|
|
435
|
+
"dd MMM yyyy",
|
|
436
|
+
)
|
|
437
|
+
: "—"}
|
|
410
438
|
</span>
|
|
411
|
-
</
|
|
412
|
-
</
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
+
</>
|
|
438
466
|
)}
|
|
439
467
|
</div>
|
|
440
468
|
)}
|
|
@@ -143,8 +143,8 @@ export function BankStatementPDFViewer({
|
|
|
143
143
|
data-slot="bank-statement-pdf-viewer"
|
|
144
144
|
>
|
|
145
145
|
{/* Navigation bar */}
|
|
146
|
-
<div className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2">
|
|
147
|
-
<div className="flex items-center gap-2">
|
|
146
|
+
<div className="flex min-w-0 shrink-0 items-center justify-between border-b border-border px-3 py-2">
|
|
147
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
148
148
|
<Button
|
|
149
149
|
variant="ghost"
|
|
150
150
|
size="icon-sm"
|
|
@@ -155,7 +155,7 @@ export function BankStatementPDFViewer({
|
|
|
155
155
|
<ChevronLeft className="size-4" />
|
|
156
156
|
</Button>
|
|
157
157
|
|
|
158
|
-
<span className="text-body-medium tabular-nums">
|
|
158
|
+
<span className="shrink-0 text-body-medium tabular-nums">
|
|
159
159
|
Account {currentAccountIndex + 1} of {accounts.length}
|
|
160
160
|
</span>
|
|
161
161
|
|
|
@@ -169,7 +169,7 @@ export function BankStatementPDFViewer({
|
|
|
169
169
|
<ChevronRight className="size-4" />
|
|
170
170
|
</Button>
|
|
171
171
|
|
|
172
|
-
<span className="ml-2 text-body-medium text-muted-foreground">
|
|
172
|
+
<span className="ml-2 min-w-0 truncate text-body-medium text-muted-foreground">
|
|
173
173
|
{accountLabel}
|
|
174
174
|
</span>
|
|
175
175
|
</div>
|
|
@@ -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
|
+
}
|