create-m5kdev 0.15.3 → 0.16.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/package.json +1 -1
- package/templates/minimal-app/apps/webapp/package.json.tpl +1 -0
- package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +10 -16
- package/templates/minimal-app/apps/webapp/src/index.css.tpl +3 -3
- package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +264 -166
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +2 -1
- package/templates/minimal-app/apps/webapp/src/hero.ts.tpl +0 -99
package/package.json
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
|
-
import { HeroUIProvider } from "@heroui/react";
|
|
2
1
|
import { AuthProvider } from "@m5kdev/frontend/modules/auth/components/AuthProvider";
|
|
3
2
|
import { DialogProvider } from "@m5kdev/web-ui/components/DialogProvider";
|
|
4
3
|
import { ThemeProvider } from "@m5kdev/web-ui/components/theme-provider";
|
|
5
4
|
import { AppLoader } from "@m5kdev/web-ui/modules/app/components/AppLoader";
|
|
6
|
-
import { useHref, useNavigate } from "react-router";
|
|
7
5
|
import { Toaster } from "sonner";
|
|
8
6
|
import { TrpcQueryProvider } from "@/components/TrpcQueryProvider";
|
|
9
7
|
import { Router } from "./Router";
|
|
10
8
|
|
|
11
9
|
export function Providers() {
|
|
12
|
-
const navigate = useNavigate();
|
|
13
|
-
|
|
14
10
|
return (
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</ThemeProvider>
|
|
26
|
-
</HeroUIProvider>
|
|
11
|
+
<ThemeProvider defaultTheme="light" storageKey="m5kdev-theme">
|
|
12
|
+
<AuthProvider loader={<AppLoader />}>
|
|
13
|
+
<TrpcQueryProvider>
|
|
14
|
+
<DialogProvider>
|
|
15
|
+
<Router />
|
|
16
|
+
</DialogProvider>
|
|
17
|
+
<Toaster richColors closeButton />
|
|
18
|
+
</TrpcQueryProvider>
|
|
19
|
+
</AuthProvider>
|
|
20
|
+
</ThemeProvider>
|
|
27
21
|
);
|
|
28
22
|
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
@import url("https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap");
|
|
4
4
|
@import "tailwindcss";
|
|
5
5
|
@import "tw-animate-css";
|
|
6
|
-
|
|
7
|
-
@plugin './hero.ts';
|
|
6
|
+
@import "@heroui/styles";
|
|
8
7
|
|
|
9
8
|
@source "./src/**/*.{ts,tsx}";
|
|
10
|
-
@source "../../node_modules/@heroui/
|
|
9
|
+
@source "../../node_modules/@heroui/react/dist/**/*.{js,mjs}";
|
|
10
|
+
@source "../../node_modules/@heroui/styles/dist/**/*.{js,mjs}";
|
|
11
11
|
@source "../../node_modules/@m5kdev/web-ui/dist/src/**/*.{js,ts,jsx,tsx}";
|
|
12
12
|
|
|
13
13
|
@custom-variant dark (&:is(.dark *));
|
|
@@ -13,19 +13,14 @@ import type {
|
|
|
13
13
|
import {
|
|
14
14
|
Button,
|
|
15
15
|
Card,
|
|
16
|
-
CardBody,
|
|
17
|
-
CardHeader,
|
|
18
16
|
Chip,
|
|
19
17
|
Input,
|
|
18
|
+
Label,
|
|
19
|
+
ListBox,
|
|
20
20
|
Modal,
|
|
21
|
-
ModalBody,
|
|
22
|
-
ModalContent,
|
|
23
|
-
ModalFooter,
|
|
24
|
-
ModalHeader,
|
|
25
21
|
Select,
|
|
26
|
-
SelectItem,
|
|
27
22
|
Skeleton,
|
|
28
|
-
|
|
23
|
+
TextArea,
|
|
29
24
|
} from "@heroui/react";
|
|
30
25
|
import { useDialog } from "@m5kdev/web-ui/components/DialogProvider";
|
|
31
26
|
import { type UseQueryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
@@ -45,6 +40,7 @@ import {
|
|
|
45
40
|
startTransition,
|
|
46
41
|
useDeferredValue,
|
|
47
42
|
useEffect,
|
|
43
|
+
useId,
|
|
48
44
|
useMemo,
|
|
49
45
|
useState,
|
|
50
46
|
} from "react";
|
|
@@ -87,6 +83,13 @@ export function PostsRoute() {
|
|
|
87
83
|
const queryClient = useQueryClient();
|
|
88
84
|
const showDialog = useDialog();
|
|
89
85
|
|
|
86
|
+
const editorTitleId = useId();
|
|
87
|
+
const editorSlugId = useId();
|
|
88
|
+
const editorExcerptId = useId();
|
|
89
|
+
const editorContentId = useId();
|
|
90
|
+
const searchFieldId = useId();
|
|
91
|
+
const statusFieldId = useId();
|
|
92
|
+
|
|
90
93
|
const [search, setSearch] = useQueryState("search", parseAsString.withDefault(""));
|
|
91
94
|
const [status, setStatus] = useQueryState<PostStatusFilter>("status", STATUS_PARSER);
|
|
92
95
|
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
|
|
@@ -266,14 +269,16 @@ export function PostsRoute() {
|
|
|
266
269
|
<p className="mt-4 max-w-2xl text-sm leading-7 text-muted-ink">{t("posts.hero.body")}</p>
|
|
267
270
|
<div className="mt-6 flex flex-wrap gap-3">
|
|
268
271
|
<Button
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
startContent={<PlusIcon className="h-4 w-4" />}
|
|
272
|
+
className="rounded-full"
|
|
273
|
+
variant="primary"
|
|
272
274
|
onPress={openCreate}
|
|
273
275
|
>
|
|
274
|
-
|
|
276
|
+
<span className="inline-flex items-center gap-2">
|
|
277
|
+
<PlusIcon className="h-4 w-4" />
|
|
278
|
+
{t("posts.hero.new")}
|
|
279
|
+
</span>
|
|
275
280
|
</Button>
|
|
276
|
-
<Chip
|
|
281
|
+
<Chip className="rounded-full" variant="soft" color="default">
|
|
277
282
|
{isFetching ? t("posts.hero.syncing") : t("posts.hero.synced")}
|
|
278
283
|
</Chip>
|
|
279
284
|
</div>
|
|
@@ -286,38 +291,72 @@ export function PostsRoute() {
|
|
|
286
291
|
<StatCard label={t("posts.stats.drafts")} value={stats.drafts} accent="stone" />
|
|
287
292
|
</div>
|
|
288
293
|
<div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_220px]">
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
294
|
+
<div className="grid gap-2">
|
|
295
|
+
<Label className="text-sm font-medium" htmlFor={searchFieldId}>
|
|
296
|
+
{t("posts.filters.searchLabel")}
|
|
297
|
+
</Label>
|
|
298
|
+
<Input
|
|
299
|
+
id={searchFieldId}
|
|
300
|
+
aria-label={t("posts.filters.searchLabel")}
|
|
301
|
+
className="rounded-lg"
|
|
302
|
+
variant="secondary"
|
|
303
|
+
placeholder={t("posts.filters.searchPlaceholder")}
|
|
304
|
+
value={search}
|
|
305
|
+
onChange={(event) => {
|
|
306
|
+
const value = event.target.value;
|
|
307
|
+
startTransition(() => {
|
|
308
|
+
void setSearch(value || null);
|
|
309
|
+
void setPage(1);
|
|
310
|
+
});
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="grid gap-2">
|
|
315
|
+
<Label className="text-sm font-medium" htmlFor={statusFieldId}>
|
|
316
|
+
{t("posts.filters.statusLabel")}
|
|
317
|
+
</Label>
|
|
318
|
+
<Select
|
|
319
|
+
aria-label={t("posts.filters.statusLabel")}
|
|
320
|
+
className="w-full"
|
|
321
|
+
variant="secondary"
|
|
322
|
+
selectedKey={status}
|
|
323
|
+
onSelectionChange={(key) => {
|
|
324
|
+
if (key === null) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const nextValue = String(key) as PostStatusFilter;
|
|
328
|
+
startTransition(() => {
|
|
329
|
+
void setStatus(nextValue);
|
|
330
|
+
void setPage(1);
|
|
331
|
+
});
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<Select.Trigger id={statusFieldId} className="rounded-lg w-full">
|
|
335
|
+
<Select.Value />
|
|
336
|
+
<Select.Indicator />
|
|
337
|
+
</Select.Trigger>
|
|
338
|
+
<Select.Popover>
|
|
339
|
+
<ListBox>
|
|
340
|
+
<ListBox.Item className="text-sm" id="all" textValue={t("posts.filters.all")}>
|
|
341
|
+
{t("posts.filters.all")}
|
|
342
|
+
<ListBox.ItemIndicator />
|
|
343
|
+
</ListBox.Item>
|
|
344
|
+
<ListBox.Item className="text-sm" id="draft" textValue={t("posts.filters.draft")}>
|
|
345
|
+
{t("posts.filters.draft")}
|
|
346
|
+
<ListBox.ItemIndicator />
|
|
347
|
+
</ListBox.Item>
|
|
348
|
+
<ListBox.Item
|
|
349
|
+
className="text-sm"
|
|
350
|
+
id="published"
|
|
351
|
+
textValue={t("posts.filters.published")}
|
|
352
|
+
>
|
|
353
|
+
{t("posts.filters.published")}
|
|
354
|
+
<ListBox.ItemIndicator />
|
|
355
|
+
</ListBox.Item>
|
|
356
|
+
</ListBox>
|
|
357
|
+
</Select.Popover>
|
|
358
|
+
</Select>
|
|
359
|
+
</div>
|
|
321
360
|
</div>
|
|
322
361
|
</div>
|
|
323
362
|
</section>
|
|
@@ -326,14 +365,14 @@ export function PostsRoute() {
|
|
|
326
365
|
<section className="grid gap-4">
|
|
327
366
|
{isLoading ? (
|
|
328
367
|
<>
|
|
329
|
-
<Skeleton className="h-44 rounded-[28px]" />
|
|
330
|
-
<Skeleton className="h-44 rounded-[28px]" />
|
|
331
|
-
<Skeleton className="h-44 rounded-[28px]" />
|
|
368
|
+
<Skeleton className="h-44 rounded-[28px]" animationType="pulse" />
|
|
369
|
+
<Skeleton className="h-44 rounded-[28px]" animationType="pulse" />
|
|
370
|
+
<Skeleton className="h-44 rounded-[28px]" animationType="pulse" />
|
|
332
371
|
</>
|
|
333
372
|
) : rows.length === 0 ? (
|
|
334
373
|
<Card className="rounded-[30px] border border-dashed border-amber-300/70 bg-panel shadow-[0_18px_40px_rgba(81,50,24,0.1)]">
|
|
335
|
-
<
|
|
336
|
-
<Chip color="
|
|
374
|
+
<Card.Content className="flex flex-col items-start gap-4 px-6 py-10">
|
|
375
|
+
<Chip color="default" variant="soft">
|
|
337
376
|
{t("posts.empty.eyebrow")}
|
|
338
377
|
</Chip>
|
|
339
378
|
<div>
|
|
@@ -342,10 +381,10 @@ export function PostsRoute() {
|
|
|
342
381
|
{t("posts.empty.body")}
|
|
343
382
|
</p>
|
|
344
383
|
</div>
|
|
345
|
-
<Button
|
|
384
|
+
<Button className="rounded-full" variant="primary" onPress={openCreate}>
|
|
346
385
|
{t("posts.empty.action")}
|
|
347
386
|
</Button>
|
|
348
|
-
</
|
|
387
|
+
</Card.Content>
|
|
349
388
|
</Card>
|
|
350
389
|
) : (
|
|
351
390
|
rows.map((row) => {
|
|
@@ -358,21 +397,33 @@ export function PostsRoute() {
|
|
|
358
397
|
return (
|
|
359
398
|
<Card
|
|
360
399
|
key={row.id}
|
|
361
|
-
|
|
362
|
-
|
|
400
|
+
role="button"
|
|
401
|
+
tabIndex={0}
|
|
402
|
+
onClick={(event) => {
|
|
403
|
+
if ((event.target as HTMLElement).closest("button")) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
setSelectedPostId(row.id);
|
|
407
|
+
}}
|
|
408
|
+
onKeyDown={(event) => {
|
|
409
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
setSelectedPostId(row.id);
|
|
412
|
+
}
|
|
413
|
+
}}
|
|
363
414
|
className={
|
|
364
415
|
isSelected
|
|
365
|
-
? "rounded-[30px] border border-emerald-300 bg-emerald-950 text-emerald-50 shadow-[0_20px_44px_rgba(31,79,70,0.24)]"
|
|
366
|
-
: "rounded-[30px] border border-white/70 bg-panel shadow-[0_18px_40px_rgba(81,50,24,0.1)]"
|
|
416
|
+
? "cursor-pointer rounded-[30px] border border-emerald-300 bg-emerald-950 text-emerald-50 shadow-[0_20px_44px_rgba(31,79,70,0.24)]"
|
|
417
|
+
: "cursor-pointer rounded-[30px] border border-white/70 bg-panel shadow-[0_18px_40px_rgba(81,50,24,0.1)]"
|
|
367
418
|
}
|
|
368
419
|
>
|
|
369
|
-
<
|
|
420
|
+
<Card.Header className="flex items-start justify-between gap-4 px-5 pt-5">
|
|
370
421
|
<div className="space-y-3">
|
|
371
422
|
<div className="flex flex-wrap items-center gap-2">
|
|
372
423
|
<Chip
|
|
373
424
|
size="sm"
|
|
374
|
-
color={row.status === "published" ? "success" : "
|
|
375
|
-
variant={isSelected ? "
|
|
425
|
+
color={row.status === "published" ? "success" : "default"}
|
|
426
|
+
variant={isSelected ? "primary" : "soft"}
|
|
376
427
|
>
|
|
377
428
|
{row.status === "published"
|
|
378
429
|
? t("posts.filters.published")
|
|
@@ -397,16 +448,13 @@ export function PostsRoute() {
|
|
|
397
448
|
</div>
|
|
398
449
|
<Button
|
|
399
450
|
isIconOnly
|
|
400
|
-
|
|
401
|
-
variant={isSelected ? "
|
|
402
|
-
className={
|
|
403
|
-
isSelected ? "bg-emerald-100 text-emerald-950" : "bg-white/80 text-ink"
|
|
404
|
-
}
|
|
451
|
+
className={`rounded-full ${isSelected ? "bg-emerald-100 text-emerald-950" : "bg-white/80 text-ink"}`}
|
|
452
|
+
variant={isSelected ? "secondary" : "ghost"}
|
|
405
453
|
>
|
|
406
454
|
<EyeIcon className="h-4 w-4" />
|
|
407
455
|
</Button>
|
|
408
|
-
</
|
|
409
|
-
<
|
|
456
|
+
</Card.Header>
|
|
457
|
+
<Card.Content className="flex flex-col gap-4 px-5 pb-5">
|
|
410
458
|
<div className="flex flex-wrap items-center gap-3 text-sm">
|
|
411
459
|
<span className={isSelected ? "text-emerald-100/80" : "text-muted-ink"}>
|
|
412
460
|
{t("posts.meta.updated")}: {formatDate(row.updatedAt ?? row.createdAt)}
|
|
@@ -419,38 +467,41 @@ export function PostsRoute() {
|
|
|
419
467
|
</div>
|
|
420
468
|
<div className="flex flex-wrap gap-2">
|
|
421
469
|
<Button
|
|
422
|
-
|
|
423
|
-
variant={isSelected ? "
|
|
424
|
-
className={isSelected ? "bg-emerald-100 text-emerald-950" : ""}
|
|
425
|
-
startContent={<PencilLineIcon className="h-4 w-4" />}
|
|
470
|
+
className={`rounded-full ${isSelected ? "bg-emerald-100 text-emerald-950" : ""}`}
|
|
471
|
+
variant={isSelected ? "secondary" : "ghost"}
|
|
426
472
|
onPress={() => openEdit(row)}
|
|
427
473
|
>
|
|
428
|
-
|
|
474
|
+
<span className="inline-flex items-center gap-2">
|
|
475
|
+
<PencilLineIcon className="h-4 w-4" />
|
|
476
|
+
{t("posts.actions.edit")}
|
|
477
|
+
</span>
|
|
429
478
|
</Button>
|
|
430
479
|
{row.status === "draft" ? (
|
|
431
480
|
<Button
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
isLoading={isPublishing}
|
|
436
|
-
startContent={<SendHorizontalIcon className="h-4 w-4" />}
|
|
481
|
+
className="rounded-full"
|
|
482
|
+
variant="secondary"
|
|
483
|
+
isPending={isPublishing}
|
|
437
484
|
onPress={() => void onPublish(row.id)}
|
|
438
485
|
>
|
|
439
|
-
|
|
486
|
+
<span className="inline-flex items-center gap-2">
|
|
487
|
+
<SendHorizontalIcon className="h-4 w-4" />
|
|
488
|
+
{t("posts.actions.publish")}
|
|
489
|
+
</span>
|
|
440
490
|
</Button>
|
|
441
491
|
) : null}
|
|
442
492
|
<Button
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
isLoading={isDeleting}
|
|
447
|
-
startContent={<Trash2Icon className="h-4 w-4" />}
|
|
493
|
+
className="rounded-full"
|
|
494
|
+
variant="danger"
|
|
495
|
+
isPending={isDeleting}
|
|
448
496
|
onPress={() => onDelete(row.id)}
|
|
449
497
|
>
|
|
450
|
-
|
|
498
|
+
<span className="inline-flex items-center gap-2">
|
|
499
|
+
<Trash2Icon className="h-4 w-4" />
|
|
500
|
+
{t("posts.actions.delete")}
|
|
501
|
+
</span>
|
|
451
502
|
</Button>
|
|
452
503
|
</div>
|
|
453
|
-
</
|
|
504
|
+
</Card.Content>
|
|
454
505
|
</Card>
|
|
455
506
|
);
|
|
456
507
|
})
|
|
@@ -466,30 +517,34 @@ export function PostsRoute() {
|
|
|
466
517
|
</p>
|
|
467
518
|
<div className="flex items-center gap-2">
|
|
468
519
|
<Button
|
|
469
|
-
|
|
470
|
-
variant="
|
|
520
|
+
className="rounded-full"
|
|
521
|
+
variant="ghost"
|
|
471
522
|
isDisabled={page <= 1}
|
|
472
|
-
startContent={<ArrowLeftIcon className="h-4 w-4" />}
|
|
473
523
|
onPress={() => {
|
|
474
524
|
startTransition(() => {
|
|
475
525
|
void setPage(Math.max(1, page - 1));
|
|
476
526
|
});
|
|
477
527
|
}}
|
|
478
528
|
>
|
|
479
|
-
|
|
529
|
+
<span className="inline-flex items-center gap-2">
|
|
530
|
+
<ArrowLeftIcon className="h-4 w-4" />
|
|
531
|
+
{t("posts.pagination.previous")}
|
|
532
|
+
</span>
|
|
480
533
|
</Button>
|
|
481
534
|
<Button
|
|
482
|
-
|
|
483
|
-
variant="
|
|
535
|
+
className="rounded-full"
|
|
536
|
+
variant="ghost"
|
|
484
537
|
isDisabled={page >= pageCount}
|
|
485
|
-
endContent={<ArrowRightIcon className="h-4 w-4" />}
|
|
486
538
|
onPress={() => {
|
|
487
539
|
startTransition(() => {
|
|
488
540
|
void setPage(Math.min(pageCount, page + 1));
|
|
489
541
|
});
|
|
490
542
|
}}
|
|
491
543
|
>
|
|
492
|
-
|
|
544
|
+
<span className="inline-flex items-center gap-2">
|
|
545
|
+
{t("posts.pagination.next")}
|
|
546
|
+
<ArrowRightIcon className="h-4 w-4" />
|
|
547
|
+
</span>
|
|
493
548
|
</Button>
|
|
494
549
|
</div>
|
|
495
550
|
</div>
|
|
@@ -498,7 +553,7 @@ export function PostsRoute() {
|
|
|
498
553
|
|
|
499
554
|
<aside className="sticky top-6 h-fit">
|
|
500
555
|
<Card className="rounded-[30px] border border-white/70 bg-panel shadow-[0_20px_44px_rgba(81,50,24,0.1)]">
|
|
501
|
-
<
|
|
556
|
+
<Card.Header className="flex items-start justify-between px-5 pt-5">
|
|
502
557
|
<div>
|
|
503
558
|
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-amber-700/80">
|
|
504
559
|
{t("posts.preview.eyebrow")}
|
|
@@ -509,26 +564,26 @@ export function PostsRoute() {
|
|
|
509
564
|
</div>
|
|
510
565
|
{selectedPost ? (
|
|
511
566
|
<Chip
|
|
512
|
-
color={selectedPost.status === "published" ? "success" : "
|
|
513
|
-
variant="
|
|
567
|
+
color={selectedPost.status === "published" ? "success" : "default"}
|
|
568
|
+
variant="soft"
|
|
514
569
|
>
|
|
515
570
|
{selectedPost.status === "published"
|
|
516
571
|
? t("posts.filters.published")
|
|
517
572
|
: t("posts.filters.draft")}
|
|
518
573
|
</Chip>
|
|
519
574
|
) : null}
|
|
520
|
-
</
|
|
521
|
-
<
|
|
575
|
+
</Card.Header>
|
|
576
|
+
<Card.Content className="flex flex-col gap-5 px-5 pb-5">
|
|
522
577
|
{selectedPost ? (
|
|
523
578
|
<>
|
|
524
579
|
<div className="rounded-[26px] border border-amber-200/70 bg-amber-50/80 p-4">
|
|
525
580
|
<p className="text-sm leading-7 text-ink/80">{selectedPost.excerpt}</p>
|
|
526
581
|
</div>
|
|
527
582
|
<div className="flex flex-wrap gap-2">
|
|
528
|
-
<Chip variant="
|
|
583
|
+
<Chip variant="soft" color="default">
|
|
529
584
|
{getReadingTime(selectedPost.content)}
|
|
530
585
|
</Chip>
|
|
531
|
-
<Chip variant="
|
|
586
|
+
<Chip variant="soft">
|
|
532
587
|
{t("posts.preview.updated")}:{" "}
|
|
533
588
|
{formatDate(selectedPost.updatedAt ?? selectedPost.createdAt)}
|
|
534
589
|
</Chip>
|
|
@@ -537,12 +592,14 @@ export function PostsRoute() {
|
|
|
537
592
|
<p>{selectedPost.content}</p>
|
|
538
593
|
</div>
|
|
539
594
|
<Button
|
|
540
|
-
|
|
541
|
-
variant="
|
|
542
|
-
startContent={<FilePenLineIcon className="h-4 w-4" />}
|
|
595
|
+
className="rounded-full"
|
|
596
|
+
variant="ghost"
|
|
543
597
|
onPress={() => openEdit(selectedPost)}
|
|
544
598
|
>
|
|
545
|
-
|
|
599
|
+
<span className="inline-flex items-center gap-2">
|
|
600
|
+
<FilePenLineIcon className="h-4 w-4" />
|
|
601
|
+
{t("posts.preview.openEditor")}
|
|
602
|
+
</span>
|
|
546
603
|
</Button>
|
|
547
604
|
</>
|
|
548
605
|
) : (
|
|
@@ -550,76 +607,117 @@ export function PostsRoute() {
|
|
|
550
607
|
{t("posts.preview.emptyBody")}
|
|
551
608
|
</div>
|
|
552
609
|
)}
|
|
553
|
-
</
|
|
610
|
+
</Card.Content>
|
|
554
611
|
</Card>
|
|
555
612
|
</aside>
|
|
556
613
|
</div>
|
|
557
614
|
|
|
558
615
|
<Modal
|
|
559
616
|
isOpen={isEditorOpen}
|
|
560
|
-
onOpenChange={
|
|
561
|
-
|
|
562
|
-
|
|
617
|
+
onOpenChange={(open) => {
|
|
618
|
+
if (!open) {
|
|
619
|
+
setIsEditorOpen(false);
|
|
620
|
+
}
|
|
621
|
+
}}
|
|
563
622
|
>
|
|
564
|
-
<
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
+
<Modal.Backdrop />
|
|
624
|
+
<Modal.Container scroll="inside" size="lg" className="max-w-5xl">
|
|
625
|
+
<Modal.Dialog>
|
|
626
|
+
<form
|
|
627
|
+
className="contents"
|
|
628
|
+
onSubmit={onSubmit}
|
|
629
|
+
>
|
|
630
|
+
<Modal.Header className="flex flex-col gap-2 px-6 pt-6">
|
|
631
|
+
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-amber-700/80">
|
|
632
|
+
{editorState.id ? t("posts.editor.editEyebrow") : t("posts.editor.newEyebrow")}
|
|
633
|
+
</p>
|
|
634
|
+
<Modal.Heading className="font-editorial text-4xl leading-none text-ink">
|
|
635
|
+
{editorState.id ? t("posts.editor.editTitle") : t("posts.editor.newTitle")}
|
|
636
|
+
</Modal.Heading>
|
|
637
|
+
</Modal.Header>
|
|
638
|
+
<Modal.Body className="grid gap-4 px-6 pb-2">
|
|
639
|
+
<div className="grid gap-2">
|
|
640
|
+
<Label className="text-sm font-medium" htmlFor={editorTitleId}>
|
|
641
|
+
{t("posts.editor.fields.title")}
|
|
642
|
+
</Label>
|
|
643
|
+
<Input
|
|
644
|
+
id={editorTitleId}
|
|
645
|
+
className="rounded-lg"
|
|
646
|
+
variant="secondary"
|
|
647
|
+
value={editorState.title}
|
|
648
|
+
onChange={(event) =>
|
|
649
|
+
setEditorState((state) => ({ ...state, title: event.target.value }))
|
|
650
|
+
}
|
|
651
|
+
isRequired
|
|
652
|
+
/>
|
|
653
|
+
</div>
|
|
654
|
+
<div className="grid gap-2">
|
|
655
|
+
<Label className="text-sm font-medium" htmlFor={editorSlugId}>
|
|
656
|
+
{t("posts.editor.fields.slug")}
|
|
657
|
+
</Label>
|
|
658
|
+
<Input
|
|
659
|
+
id={editorSlugId}
|
|
660
|
+
className="rounded-lg"
|
|
661
|
+
variant="secondary"
|
|
662
|
+
value={editorState.slug}
|
|
663
|
+
onChange={(event) =>
|
|
664
|
+
setEditorState((state) => ({ ...state, slug: event.target.value }))
|
|
665
|
+
}
|
|
666
|
+
/>
|
|
667
|
+
</div>
|
|
668
|
+
<div className="grid gap-2">
|
|
669
|
+
<Label className="text-sm font-medium" htmlFor={editorExcerptId}>
|
|
670
|
+
{t("posts.editor.fields.excerpt")}
|
|
671
|
+
</Label>
|
|
672
|
+
<TextArea
|
|
673
|
+
id={editorExcerptId}
|
|
674
|
+
className="rounded-lg min-h-[5.5rem]"
|
|
675
|
+
variant="secondary"
|
|
676
|
+
rows={3}
|
|
677
|
+
value={editorState.excerpt}
|
|
678
|
+
onChange={(event) =>
|
|
679
|
+
setEditorState((state) => ({ ...state, excerpt: event.target.value }))
|
|
680
|
+
}
|
|
681
|
+
/>
|
|
682
|
+
</div>
|
|
683
|
+
<div className="grid gap-2">
|
|
684
|
+
<Label className="text-sm font-medium" htmlFor={editorContentId}>
|
|
685
|
+
{t("posts.editor.fields.content")}
|
|
686
|
+
</Label>
|
|
687
|
+
<TextArea
|
|
688
|
+
id={editorContentId}
|
|
689
|
+
className="rounded-lg min-h-[12rem]"
|
|
690
|
+
variant="secondary"
|
|
691
|
+
rows={10}
|
|
692
|
+
value={editorState.content}
|
|
693
|
+
onChange={(event) =>
|
|
694
|
+
setEditorState((state) => ({ ...state, content: event.target.value }))
|
|
695
|
+
}
|
|
696
|
+
isRequired
|
|
697
|
+
/>
|
|
698
|
+
</div>
|
|
699
|
+
</Modal.Body>
|
|
700
|
+
<Modal.Footer className="px-6 pb-6">
|
|
701
|
+
<Button
|
|
702
|
+
className="rounded-full"
|
|
703
|
+
variant="tertiary"
|
|
704
|
+
type="button"
|
|
705
|
+
onPress={() => setIsEditorOpen(false)}
|
|
706
|
+
>
|
|
707
|
+
{t("posts.editor.cancel")}
|
|
708
|
+
</Button>
|
|
709
|
+
<Button
|
|
710
|
+
className="rounded-full"
|
|
711
|
+
variant="primary"
|
|
712
|
+
type="submit"
|
|
713
|
+
isPending={createMutation.isPending || updateMutation.isPending}
|
|
714
|
+
>
|
|
715
|
+
{editorState.id ? t("posts.editor.save") : t("posts.editor.create")}
|
|
716
|
+
</Button>
|
|
717
|
+
</Modal.Footer>
|
|
718
|
+
</form>
|
|
719
|
+
</Modal.Dialog>
|
|
720
|
+
</Modal.Container>
|
|
623
721
|
</Modal>
|
|
624
722
|
</div>
|
|
625
723
|
);
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { heroui } from "@heroui/react";
|
|
2
|
-
|
|
3
|
-
export default heroui({
|
|
4
|
-
themes: {
|
|
5
|
-
light: {
|
|
6
|
-
colors: {
|
|
7
|
-
primary: {
|
|
8
|
-
"50": "#edf7f4",
|
|
9
|
-
"100": "#cfeadd",
|
|
10
|
-
"200": "#adddc5",
|
|
11
|
-
"300": "#88cfac",
|
|
12
|
-
"400": "#61c091",
|
|
13
|
-
"500": "#1f4f46",
|
|
14
|
-
"600": "#19433b",
|
|
15
|
-
"700": "#12362f",
|
|
16
|
-
"800": "#0c2923",
|
|
17
|
-
"900": "#051a16",
|
|
18
|
-
DEFAULT: "#1f4f46",
|
|
19
|
-
foreground: "#fff8ef",
|
|
20
|
-
},
|
|
21
|
-
secondary: {
|
|
22
|
-
"50": "#fff4e5",
|
|
23
|
-
"100": "#fce2bd",
|
|
24
|
-
"200": "#f8cf95",
|
|
25
|
-
"300": "#f4bc6d",
|
|
26
|
-
"400": "#efaa45",
|
|
27
|
-
"500": "#c7782d",
|
|
28
|
-
"600": "#a16024",
|
|
29
|
-
"700": "#7b491b",
|
|
30
|
-
"800": "#553112",
|
|
31
|
-
"900": "#301a09",
|
|
32
|
-
DEFAULT: "#c7782d",
|
|
33
|
-
foreground: "#fff9f2",
|
|
34
|
-
},
|
|
35
|
-
background: "#f4ede2",
|
|
36
|
-
foreground: "#171314",
|
|
37
|
-
content1: {
|
|
38
|
-
DEFAULT: "#fffaf1",
|
|
39
|
-
foreground: "#171314",
|
|
40
|
-
},
|
|
41
|
-
content2: {
|
|
42
|
-
DEFAULT: "#f4ede2",
|
|
43
|
-
foreground: "#171314",
|
|
44
|
-
},
|
|
45
|
-
focus: "#1f4f46",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
dark: {
|
|
49
|
-
colors: {
|
|
50
|
-
primary: {
|
|
51
|
-
"50": "#e2f3ef",
|
|
52
|
-
"100": "#beded7",
|
|
53
|
-
"200": "#98c8bc",
|
|
54
|
-
"300": "#71b2a1",
|
|
55
|
-
"400": "#4b9c86",
|
|
56
|
-
"500": "#2d8470",
|
|
57
|
-
"600": "#236959",
|
|
58
|
-
"700": "#194e42",
|
|
59
|
-
"800": "#10332b",
|
|
60
|
-
"900": "#061815",
|
|
61
|
-
DEFAULT: "#71b2a1",
|
|
62
|
-
foreground: "#071613",
|
|
63
|
-
},
|
|
64
|
-
secondary: {
|
|
65
|
-
"50": "#fff4e5",
|
|
66
|
-
"100": "#f6dcb0",
|
|
67
|
-
"200": "#eec379",
|
|
68
|
-
"300": "#e6aa42",
|
|
69
|
-
"400": "#d88d27",
|
|
70
|
-
"500": "#b97221",
|
|
71
|
-
"600": "#935a1b",
|
|
72
|
-
"700": "#6d4214",
|
|
73
|
-
"800": "#472b0d",
|
|
74
|
-
"900": "#231406",
|
|
75
|
-
DEFAULT: "#e6aa42",
|
|
76
|
-
foreground: "#211306",
|
|
77
|
-
},
|
|
78
|
-
background: "#12100f",
|
|
79
|
-
foreground: "#f7f1e8",
|
|
80
|
-
content1: {
|
|
81
|
-
DEFAULT: "#1d1917",
|
|
82
|
-
foreground: "#f7f1e8",
|
|
83
|
-
},
|
|
84
|
-
content2: {
|
|
85
|
-
DEFAULT: "#26211f",
|
|
86
|
-
foreground: "#f7f1e8",
|
|
87
|
-
},
|
|
88
|
-
focus: "#71b2a1",
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
layout: {
|
|
93
|
-
radius: {
|
|
94
|
-
small: "18px",
|
|
95
|
-
medium: "24px",
|
|
96
|
-
large: "32px",
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
});
|