@betterstart/cli 0.1.28 → 0.1.30
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/dist/{chunk-SAPJG4NO.js → chunk-6JCWMKSY.js} +7 -4
- package/dist/{chunk-SAPJG4NO.js.map → chunk-6JCWMKSY.js.map} +1 -1
- package/dist/cli.js +749 -866
- package/dist/cli.js.map +1 -1
- package/dist/drizzle-config-EDKOEZ6G.js +7 -0
- package/package.json +1 -1
- package/templates/ui/accordion.tsx +73 -42
- package/templates/ui/alert-dialog.tsx +155 -90
- package/templates/ui/alert.tsx +46 -26
- package/templates/ui/aspect-ratio.tsx +4 -2
- package/templates/ui/avatar.tsx +92 -43
- package/templates/ui/badge.tsx +27 -12
- package/templates/ui/breadcrumb.tsx +63 -60
- package/templates/ui/button-group.tsx +8 -8
- package/templates/ui/button.tsx +44 -26
- package/templates/ui/calendar.tsx +43 -34
- package/templates/ui/card.tsx +71 -34
- package/templates/ui/carousel.tsx +111 -115
- package/templates/ui/chart.tsx +197 -207
- package/templates/ui/checkbox.tsx +21 -20
- package/templates/ui/collapsible.tsx +14 -4
- package/templates/ui/combobox.tsx +272 -0
- package/templates/ui/command.tsx +139 -101
- package/templates/ui/context-menu.tsx +214 -156
- package/templates/ui/dialog.tsx +118 -77
- package/templates/ui/direction.tsx +20 -0
- package/templates/ui/drawer.tsx +89 -69
- package/templates/ui/dropdown-menu.tsx +228 -164
- package/templates/ui/empty.tsx +8 -5
- package/templates/ui/field.tsx +25 -32
- package/templates/ui/hover-card.tsx +29 -20
- package/templates/ui/input-group.tsx +20 -37
- package/templates/ui/input-otp.tsx +57 -42
- package/templates/ui/input.tsx +14 -17
- package/templates/ui/item.tsx +27 -17
- package/templates/ui/kbd.tsx +1 -3
- package/templates/ui/label.tsx +14 -14
- package/templates/ui/markdown-editor.tsx +1 -1
- package/templates/ui/menubar.tsx +220 -188
- package/templates/ui/native-select.tsx +42 -0
- package/templates/ui/navigation-menu.tsx +130 -90
- package/templates/ui/pagination.tsx +88 -73
- package/templates/ui/popover.tsx +67 -26
- package/templates/ui/progress.tsx +24 -18
- package/templates/ui/radio-group.tsx +26 -20
- package/templates/ui/resizable.tsx +29 -29
- package/templates/ui/scroll-area.tsx +47 -38
- package/templates/ui/select.tsx +158 -125
- package/templates/ui/separator.tsx +21 -19
- package/templates/ui/sheet.tsx +104 -95
- package/templates/ui/sidebar.tsx +77 -183
- package/templates/ui/skeleton.tsx +8 -2
- package/templates/ui/slider.tsx +46 -17
- package/templates/ui/sonner.tsx +19 -9
- package/templates/ui/spinner.tsx +2 -2
- package/templates/ui/switch.tsx +24 -20
- package/templates/ui/table.tsx +68 -73
- package/templates/ui/tabs.tsx +71 -46
- package/templates/ui/textarea.tsx +13 -16
- package/templates/ui/toggle-group.tsx +57 -28
- package/templates/ui/toggle.tsx +21 -20
- package/templates/ui/tooltip.tsx +44 -23
- package/dist/drizzle-config-KISB26BA.js +0 -7
- package/templates/ui/use-mobile.tsx +0 -19
- /package/dist/{drizzle-config-KISB26BA.js.map → drizzle-config-EDKOEZ6G.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
drizzleConfigTemplate
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6JCWMKSY.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command as Command8 } from "commander";
|
|
@@ -1404,6 +1404,7 @@ function generatePageContent(pascal, kebab, camel, label) {
|
|
|
1404
1404
|
|
|
1405
1405
|
import type { ${pascal}SubmissionData } from '@cms/actions/${kebab}-form'
|
|
1406
1406
|
import { deleteBulk${pascal}Submissions } from '@cms/actions/${kebab}-form'
|
|
1407
|
+
import { PageHeader } from '@cms/components/shared/page-header'
|
|
1407
1408
|
import { Button } from '@cms/components/ui/button'
|
|
1408
1409
|
import { Input } from '@cms/components/ui/input'
|
|
1409
1410
|
import {
|
|
@@ -1417,11 +1418,11 @@ import {
|
|
|
1417
1418
|
AlertDialogTitle,
|
|
1418
1419
|
AlertDialogTrigger,
|
|
1419
1420
|
} from '@cms/components/ui/alert-dialog'
|
|
1420
|
-
import { PageHeader } from '@cms/components/shared/page-header'
|
|
1421
1421
|
import { useQueryClient } from '@tanstack/react-query'
|
|
1422
1422
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
1423
|
-
import { Search, Settings, Trash2 } from 'lucide-react'
|
|
1423
|
+
import { ChevronLeft, Search, Settings, Trash2 } from 'lucide-react'
|
|
1424
1424
|
import Link from 'next/link'
|
|
1425
|
+
import { useRouter } from 'next/navigation'
|
|
1425
1426
|
import { parseAsString, useQueryState } from 'nuqs'
|
|
1426
1427
|
import { startTransition, useCallback, useState, useTransition } from 'react'
|
|
1427
1428
|
import { toast } from 'sonner'
|
|
@@ -1434,6 +1435,7 @@ interface ${pascal}SubmissionsPageContentProps<TValue> {
|
|
|
1434
1435
|
export function ${pascal}SubmissionsPageContent<TValue>({
|
|
1435
1436
|
columns,
|
|
1436
1437
|
}: ${pascal}SubmissionsPageContentProps<TValue>) {
|
|
1438
|
+
const router = useRouter()
|
|
1437
1439
|
const queryClient = useQueryClient()
|
|
1438
1440
|
const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''))
|
|
1439
1441
|
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
|
@@ -1470,33 +1472,21 @@ export function ${pascal}SubmissionsPageContent<TValue>({
|
|
|
1470
1472
|
|
|
1471
1473
|
return (
|
|
1472
1474
|
<>
|
|
1473
|
-
<
|
|
1474
|
-
<
|
|
1475
|
-
<
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
defaultValue={search}
|
|
1484
|
-
className="w-64 pl-9"
|
|
1485
|
-
/>
|
|
1486
|
-
</div>
|
|
1487
|
-
<Button type="submit" variant="outline">Search</Button>
|
|
1488
|
-
</form>
|
|
1489
|
-
<Button variant="outline" asChild>
|
|
1490
|
-
<Link href="/cms/forms/${kebab}/settings">
|
|
1491
|
-
<Settings className="size-3.5" />
|
|
1492
|
-
Settings
|
|
1493
|
-
</Link>
|
|
1494
|
-
</Button>
|
|
1475
|
+
<PageHeader title="${label}" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} search={<form action={searchAction} className="flex items-center gap-2 relative">
|
|
1476
|
+
<Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
|
|
1477
|
+
<Input
|
|
1478
|
+
key={search}
|
|
1479
|
+
name="search"
|
|
1480
|
+
placeholder="Search submissions..."
|
|
1481
|
+
defaultValue={search}
|
|
1482
|
+
className="w-64 pl-9 bg-white rounded-full"
|
|
1483
|
+
/>
|
|
1484
|
+
</form>} actions={<div className="flex items-center gap-2">
|
|
1495
1485
|
{selectedIds.length > 0 && (
|
|
1496
1486
|
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
1497
1487
|
<AlertDialogTrigger asChild>
|
|
1498
|
-
<Button variant="destructive">
|
|
1499
|
-
<Trash2 className="size-3.5" />
|
|
1488
|
+
<Button variant="destructive" size="default">
|
|
1489
|
+
<Trash2 className="size-3.5 -ml-0.5" strokeWidth={2} />
|
|
1500
1490
|
Delete {selectedIds.length}
|
|
1501
1491
|
</Button>
|
|
1502
1492
|
</AlertDialogTrigger>
|
|
@@ -1521,9 +1511,13 @@ export function ${pascal}SubmissionsPageContent<TValue>({
|
|
|
1521
1511
|
</AlertDialogContent>
|
|
1522
1512
|
</AlertDialog>
|
|
1523
1513
|
)}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1514
|
+
<Button variant="outline" asChild>
|
|
1515
|
+
<Link href="/cms/forms/${kebab}/settings">
|
|
1516
|
+
<Settings className="size-3.5" />
|
|
1517
|
+
Settings
|
|
1518
|
+
</Link>
|
|
1519
|
+
</Button>
|
|
1520
|
+
</div>} />
|
|
1527
1521
|
<main className="space-y-4 p-6">
|
|
1528
1522
|
<${pascal}SubmissionsTable
|
|
1529
1523
|
columns={columns}
|
|
@@ -1597,8 +1591,9 @@ function generateViewPage(pascal, kebab, fields, label, includeDynamic) {
|
|
|
1597
1591
|
</div>
|
|
1598
1592
|
)}` : "";
|
|
1599
1593
|
return `import { get${pascal}Submission } from '@cms/actions/${kebab}-form'
|
|
1594
|
+
import { PageHeader } from '@cms/components/shared/page-header'
|
|
1600
1595
|
import { Button } from '@cms/components/ui/button'
|
|
1601
|
-
import {
|
|
1596
|
+
import { ChevronLeft } from 'lucide-react'
|
|
1602
1597
|
import Link from 'next/link'
|
|
1603
1598
|
import { notFound } from 'next/navigation'
|
|
1604
1599
|
|
|
@@ -1615,21 +1610,9 @@ export default async function Page({ params }: PageProps) {
|
|
|
1615
1610
|
}
|
|
1616
1611
|
|
|
1617
1612
|
return (
|
|
1618
|
-
|
|
1619
|
-
<
|
|
1620
|
-
|
|
1621
|
-
<h1 className="text-2xl font-bold">${label} Submission</h1>
|
|
1622
|
-
<p className="text-muted-foreground">Viewing submission #{id}</p>
|
|
1623
|
-
</div>
|
|
1624
|
-
<Button variant="outline" asChild>
|
|
1625
|
-
<Link href="/cms/forms/${kebab}">
|
|
1626
|
-
<ArrowLeft className="size-4" />
|
|
1627
|
-
Back to ${label}
|
|
1628
|
-
</Link>
|
|
1629
|
-
</Button>
|
|
1630
|
-
</div>
|
|
1631
|
-
|
|
1632
|
-
<div className="rounded-lg border p-6 space-y-4">
|
|
1613
|
+
<>
|
|
1614
|
+
<PageHeader title="${label} Submission" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/forms/${kebab}"><ChevronLeft /></Link></Button>} />
|
|
1615
|
+
<div className="rounded-lg border p-6 mx-6 mt-6 space-y-4">
|
|
1633
1616
|
${fieldItems}${customFieldsSection}
|
|
1634
1617
|
<div className="space-y-1 border-t pt-4">
|
|
1635
1618
|
<p className="text-sm font-medium text-muted-foreground">Submitted At</p>
|
|
@@ -1641,7 +1624,7 @@ ${fieldItems}${customFieldsSection}
|
|
|
1641
1624
|
</p>
|
|
1642
1625
|
</div>
|
|
1643
1626
|
</div>
|
|
1644
|
-
|
|
1627
|
+
</>
|
|
1645
1628
|
)
|
|
1646
1629
|
}
|
|
1647
1630
|
`;
|
|
@@ -1654,17 +1637,19 @@ import {
|
|
|
1654
1637
|
testFormWebhook,
|
|
1655
1638
|
upsertFormSettings,
|
|
1656
1639
|
} from '@cms/actions/form-settings'
|
|
1640
|
+
import { PageHeader } from '@cms/components/shared/page-header'
|
|
1657
1641
|
import { Button } from '@cms/components/ui/button'
|
|
1658
1642
|
import { Input } from '@cms/components/ui/input'
|
|
1659
1643
|
import { Label } from '@cms/components/ui/label'
|
|
1660
1644
|
import { Switch } from '@cms/components/ui/switch'
|
|
1661
1645
|
import { Textarea } from '@cms/components/ui/textarea'
|
|
1662
|
-
import {
|
|
1663
|
-
import
|
|
1646
|
+
import { ChevronLeft, Loader2 } from 'lucide-react'
|
|
1647
|
+
import { useRouter } from 'next/navigation'
|
|
1664
1648
|
import { useEffect, useState, useTransition } from 'react'
|
|
1665
1649
|
import { toast } from 'sonner'
|
|
1666
1650
|
|
|
1667
1651
|
export default function ${pascal}SettingsPage() {
|
|
1652
|
+
const router = useRouter()
|
|
1668
1653
|
const [notificationEmails, setNotificationEmails] = useState('')
|
|
1669
1654
|
const [webhookUrl, setWebhookUrl] = useState('')
|
|
1670
1655
|
const [webhookEnabled, setWebhookEnabled] = useState(false)
|
|
@@ -1718,23 +1703,9 @@ export default function ${pascal}SettingsPage() {
|
|
|
1718
1703
|
}
|
|
1719
1704
|
|
|
1720
1705
|
return (
|
|
1721
|
-
|
|
1722
|
-
<
|
|
1723
|
-
|
|
1724
|
-
<h1 className="text-2xl font-bold">${label} Settings</h1>
|
|
1725
|
-
<p className="text-muted-foreground">
|
|
1726
|
-
Configure notifications and webhooks for this form
|
|
1727
|
-
</p>
|
|
1728
|
-
</div>
|
|
1729
|
-
<Button variant="outline" asChild>
|
|
1730
|
-
<Link href="/cms/forms/${kebab}">
|
|
1731
|
-
<ArrowLeft className="size-4" />
|
|
1732
|
-
Back to ${label}
|
|
1733
|
-
</Link>
|
|
1734
|
-
</Button>
|
|
1735
|
-
</div>
|
|
1736
|
-
|
|
1737
|
-
<div className="space-y-6 rounded-lg border p-6">
|
|
1706
|
+
<>
|
|
1707
|
+
<PageHeader title="${label} Settings" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} />
|
|
1708
|
+
<div className="space-y-6 rounded-lg border p-6 mx-6 mt-6">
|
|
1738
1709
|
<div className="space-y-2">
|
|
1739
1710
|
<Label htmlFor="notificationEmails">Notification Emails</Label>
|
|
1740
1711
|
<Textarea
|
|
@@ -1804,7 +1775,7 @@ export default function ${pascal}SettingsPage() {
|
|
|
1804
1775
|
</Button>
|
|
1805
1776
|
</div>
|
|
1806
1777
|
</div>
|
|
1807
|
-
|
|
1778
|
+
</>
|
|
1808
1779
|
)
|
|
1809
1780
|
}
|
|
1810
1781
|
`;
|
|
@@ -1866,55 +1837,47 @@ function parseSingleItem(str) {
|
|
|
1866
1837
|
const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
|
|
1867
1838
|
const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
|
|
1868
1839
|
const iconMatch = str.match(/icon:\s*(\w+)/);
|
|
1840
|
+
const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
|
|
1869
1841
|
if (!labelMatch || !hrefMatch) return null;
|
|
1870
1842
|
const item = { label: labelMatch[1], href: hrefMatch[1] };
|
|
1871
1843
|
if (iconMatch) item.icon = iconMatch[1];
|
|
1872
|
-
|
|
1873
|
-
if (childrenMatch) {
|
|
1874
|
-
item.children = parseItemsBlock(childrenMatch[1]);
|
|
1875
|
-
}
|
|
1844
|
+
if (groupMatch) item.group = groupMatch[1];
|
|
1876
1845
|
return item;
|
|
1877
1846
|
}
|
|
1878
1847
|
function generateNavigationCode(items, iconImports) {
|
|
1879
1848
|
const lines = [];
|
|
1880
|
-
lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
|
|
1881
1849
|
lines.push("import type { LucideIcon } from 'lucide-react'");
|
|
1850
|
+
lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
|
|
1882
1851
|
lines.push("");
|
|
1883
1852
|
lines.push("export interface CmsNavigationItem {");
|
|
1884
1853
|
lines.push(" label: string");
|
|
1885
1854
|
lines.push(" href: string");
|
|
1886
1855
|
lines.push(" icon?: LucideIcon");
|
|
1887
|
-
lines.push("
|
|
1856
|
+
lines.push(" group?: string");
|
|
1888
1857
|
lines.push("}");
|
|
1889
1858
|
lines.push("");
|
|
1890
1859
|
lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
|
|
1891
1860
|
for (let i = 0; i < items.length; i++) {
|
|
1892
|
-
appendItem(lines, items[i],
|
|
1861
|
+
appendItem(lines, items[i], i === items.length - 1);
|
|
1893
1862
|
}
|
|
1894
1863
|
lines.push("]");
|
|
1895
1864
|
lines.push("");
|
|
1896
1865
|
return lines.join("\n");
|
|
1897
1866
|
}
|
|
1898
|
-
function appendItem(lines, item,
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
lines.push(
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
lines.push(
|
|
1910
|
-
|
|
1911
|
-
}
|
|
1912
|
-
lines.push(`${pad}{`);
|
|
1913
|
-
lines.push(`${pad} label: '${item.label}',`);
|
|
1914
|
-
lines.push(`${pad} href: '${item.href}'${item.icon ? "," : ""}`);
|
|
1915
|
-
if (item.icon) lines.push(`${pad} icon: ${item.icon}`);
|
|
1916
|
-
lines.push(`${pad}}${isLast ? "" : ","}`);
|
|
1917
|
-
}
|
|
1867
|
+
function appendItem(lines, item, isLast) {
|
|
1868
|
+
lines.push(" {");
|
|
1869
|
+
lines.push(` label: '${item.label}',`);
|
|
1870
|
+
const hasMore = item.icon != null || item.group != null;
|
|
1871
|
+
lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
|
|
1872
|
+
if (item.icon && item.group) {
|
|
1873
|
+
lines.push(` icon: ${item.icon},`);
|
|
1874
|
+
lines.push(` group: '${item.group}'`);
|
|
1875
|
+
} else if (item.icon) {
|
|
1876
|
+
lines.push(` icon: ${item.icon}`);
|
|
1877
|
+
} else if (item.group) {
|
|
1878
|
+
lines.push(` group: '${item.group}'`);
|
|
1879
|
+
}
|
|
1880
|
+
lines.push(` }${isLast ? "" : ","}`);
|
|
1918
1881
|
}
|
|
1919
1882
|
function toKebabCase2(str) {
|
|
1920
1883
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
@@ -1929,45 +1892,35 @@ function updateFormNavigation(schema, cwd, cmsDir, options = {}) {
|
|
|
1929
1892
|
items = parsed.items;
|
|
1930
1893
|
iconImports = parsed.iconImports;
|
|
1931
1894
|
}
|
|
1932
|
-
let formsGroup = items.find((item) => item.label === "Forms");
|
|
1933
|
-
if (!formsGroup) {
|
|
1934
|
-
formsGroup = {
|
|
1935
|
-
label: "Forms",
|
|
1936
|
-
href: "#",
|
|
1937
|
-
icon: "FileInput",
|
|
1938
|
-
children: []
|
|
1939
|
-
};
|
|
1940
|
-
items.push(formsGroup);
|
|
1941
|
-
}
|
|
1942
|
-
if (!formsGroup.children) {
|
|
1943
|
-
formsGroup.children = [];
|
|
1944
|
-
}
|
|
1945
1895
|
const kebab = toKebabCase2(schema.name);
|
|
1946
1896
|
const formHref = `/cms/forms/${kebab}`;
|
|
1947
|
-
const existingIndex =
|
|
1948
|
-
const
|
|
1897
|
+
const existingIndex = items.findIndex((item) => item.href === formHref);
|
|
1898
|
+
const newItem = {
|
|
1949
1899
|
label: schema.label,
|
|
1950
1900
|
href: formHref,
|
|
1951
|
-
icon: "Inbox"
|
|
1901
|
+
icon: "Inbox",
|
|
1902
|
+
group: "Forms"
|
|
1952
1903
|
};
|
|
1953
1904
|
if (existingIndex >= 0) {
|
|
1954
1905
|
if (options.force) {
|
|
1955
|
-
|
|
1906
|
+
items[existingIndex] = newItem;
|
|
1956
1907
|
} else {
|
|
1957
1908
|
return { files: [] };
|
|
1958
1909
|
}
|
|
1959
1910
|
} else {
|
|
1960
|
-
|
|
1961
|
-
formsGroup.children.sort((a, b) => a.label.localeCompare(b.label));
|
|
1911
|
+
items.push(newItem);
|
|
1962
1912
|
}
|
|
1963
1913
|
const dashboard = items.find((item) => item.href === "/cms");
|
|
1964
1914
|
const others = items.filter((item) => item.href !== "/cms");
|
|
1965
|
-
others.sort((a, b) =>
|
|
1915
|
+
others.sort((a, b) => {
|
|
1916
|
+
if (!a.group && b.group) return -1;
|
|
1917
|
+
if (a.group && !b.group) return 1;
|
|
1918
|
+
if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
|
|
1919
|
+
return a.label.localeCompare(b.label);
|
|
1920
|
+
});
|
|
1966
1921
|
items = [...dashboard ? [dashboard] : [], ...others];
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
iconImports.push(icon);
|
|
1970
|
-
}
|
|
1922
|
+
if (!iconImports.includes("Inbox")) {
|
|
1923
|
+
iconImports.push("Inbox");
|
|
1971
1924
|
}
|
|
1972
1925
|
iconImports.sort();
|
|
1973
1926
|
const dir = path5.dirname(navFilePath);
|
|
@@ -2423,7 +2376,7 @@ function generateFormComponent(schema, cwd, cmsDir, options) {
|
|
|
2423
2376
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2424
2377
|
import { useState } from 'react'
|
|
2425
2378
|
import { useForm } from 'react-hook-form'
|
|
2426
|
-
import { z } from 'zod'
|
|
2379
|
+
import { z } from 'zod/v3'
|
|
2427
2380
|
import { create${pascal}Submission } from '@cms/actions/${formName}-form'
|
|
2428
2381
|
import { Button } from '@cms/components/ui/button'
|
|
2429
2382
|
import {
|
|
@@ -4517,7 +4470,7 @@ function generateCreatePage(schema, cwd, pagesDir, options = {}) {
|
|
|
4517
4470
|
const Singular = toPascalCase8(singular);
|
|
4518
4471
|
const singLabel = singularizeLabel(schema.label);
|
|
4519
4472
|
const kebabName = toKebabCase4(schema.name);
|
|
4520
|
-
const content = `import {
|
|
4473
|
+
const content = `import { ChevronLeft } from 'lucide-react'
|
|
4521
4474
|
import Link from 'next/link'
|
|
4522
4475
|
import { connection } from 'next/server'
|
|
4523
4476
|
import { PageHeader } from '@cms/components/shared/page-header'
|
|
@@ -4528,23 +4481,12 @@ export default async function Create${Singular}Page() {
|
|
|
4528
4481
|
await connection()
|
|
4529
4482
|
|
|
4530
4483
|
return (
|
|
4531
|
-
|
|
4532
|
-
<
|
|
4533
|
-
|
|
4534
|
-
title="Create ${singLabel}"
|
|
4535
|
-
description="Add a new ${singLabel.toLowerCase()} to the system"
|
|
4536
|
-
/>
|
|
4537
|
-
<Button variant="outline" asChild>
|
|
4538
|
-
<Link href="/cms/${schema.name}">
|
|
4539
|
-
<ArrowLeft className="size-4" />
|
|
4540
|
-
Back to ${schema.label}
|
|
4541
|
-
</Link>
|
|
4542
|
-
</Button>
|
|
4543
|
-
</div>
|
|
4544
|
-
<main className="container mx-auto max-w-5xl p-6">
|
|
4484
|
+
<>
|
|
4485
|
+
<PageHeader title="Create ${singLabel}" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/${schema.name}"><ChevronLeft /></Link></Button>} />
|
|
4486
|
+
<main className="container mx-auto max-w-5xl p-6 pb-20">
|
|
4545
4487
|
<${Singular}Form key={Date.now()} />
|
|
4546
4488
|
</main>
|
|
4547
|
-
|
|
4489
|
+
</>
|
|
4548
4490
|
)
|
|
4549
4491
|
}
|
|
4550
4492
|
`;
|
|
@@ -4838,7 +4780,7 @@ function generateEditPage(schema, cwd, pagesDir, options = {}) {
|
|
|
4838
4780
|
const Singular = toPascalCase10(singular);
|
|
4839
4781
|
const camelSingular = toCamelCase6(singular);
|
|
4840
4782
|
const kebabName = toKebabCase5(schema.name);
|
|
4841
|
-
const content = `import {
|
|
4783
|
+
const content = `import { ChevronLeft } from 'lucide-react'
|
|
4842
4784
|
import Link from 'next/link'
|
|
4843
4785
|
import { notFound } from 'next/navigation'
|
|
4844
4786
|
import { PageHeader } from '@cms/components/shared/page-header'
|
|
@@ -4861,23 +4803,12 @@ export default async function Edit${Singular}Page({ params }: PageProps) {
|
|
|
4861
4803
|
}
|
|
4862
4804
|
|
|
4863
4805
|
return (
|
|
4864
|
-
|
|
4865
|
-
<
|
|
4866
|
-
|
|
4867
|
-
title="Edit ${Singular}"
|
|
4868
|
-
description="Update ${singular} information"
|
|
4869
|
-
/>
|
|
4870
|
-
<Button variant="outline" asChild>
|
|
4871
|
-
<Link href="/cms/${schema.name}">
|
|
4872
|
-
<ArrowLeft className="size-4" />
|
|
4873
|
-
Back to ${schema.label}
|
|
4874
|
-
</Link>
|
|
4875
|
-
</Button>
|
|
4876
|
-
</div>
|
|
4877
|
-
<main className="container mx-auto max-w-5xl p-6">
|
|
4806
|
+
<>
|
|
4807
|
+
<PageHeader title="Edit ${Singular}" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/${schema.name}"><ChevronLeft /></Link></Button>} />
|
|
4808
|
+
<main className="container mx-auto max-w-5xl p-6 pb-20">
|
|
4878
4809
|
<${Singular}Form key={${camelSingular}.id} initialData={${camelSingular}} />
|
|
4879
4810
|
</main>
|
|
4880
|
-
|
|
4811
|
+
</>
|
|
4881
4812
|
)
|
|
4882
4813
|
}
|
|
4883
4814
|
`;
|
|
@@ -5720,9 +5651,14 @@ function generateForm(schema, cwd, pagesDir, options = {}) {
|
|
|
5720
5651
|
function collectListFields(fields) {
|
|
5721
5652
|
for (const f of fields) {
|
|
5722
5653
|
if (f.type === "list" && f.fields && f.fields.length > 0 && !f.hidden) {
|
|
5723
|
-
|
|
5654
|
+
listFieldsWithNested.push(f);
|
|
5724
5655
|
}
|
|
5725
5656
|
if (f.type === "group" && f.fields) collectListFields(f.fields);
|
|
5657
|
+
if (f.type === "tabs" && f.tabs) {
|
|
5658
|
+
for (const tab of f.tabs) {
|
|
5659
|
+
if (tab.fields) collectListFields(tab.fields);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5726
5662
|
}
|
|
5727
5663
|
}
|
|
5728
5664
|
collectListFields(allFormFields);
|
|
@@ -5868,7 +5804,7 @@ import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
|
|
|
5868
5804
|
import { useRouter } from 'next/navigation'
|
|
5869
5805
|
import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
|
|
5870
5806
|
import { toast } from 'sonner'
|
|
5871
|
-
import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
5807
|
+
import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
5872
5808
|
${relHookImports ? `${relHookImports}
|
|
5873
5809
|
` : ""}${uiImports.join("\n")}
|
|
5874
5810
|
import type {
|
|
@@ -6048,9 +5984,14 @@ function generateSingleForm(schema, cwd, pagesDir, options = {}) {
|
|
|
6048
5984
|
function collectListFieldsSingle(fields) {
|
|
6049
5985
|
for (const f of fields) {
|
|
6050
5986
|
if (f.type === "list" && f.fields && f.fields.length > 0 && !f.hidden) {
|
|
6051
|
-
|
|
5987
|
+
listFieldsWithNested.push(f);
|
|
6052
5988
|
}
|
|
6053
5989
|
if (f.type === "group" && f.fields) collectListFieldsSingle(f.fields);
|
|
5990
|
+
if (f.type === "tabs" && f.tabs) {
|
|
5991
|
+
for (const tab of f.tabs) {
|
|
5992
|
+
if (tab.fields) collectListFieldsSingle(tab.fields);
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
6054
5995
|
}
|
|
6055
5996
|
}
|
|
6056
5997
|
collectListFieldsSingle(allFormFields);
|
|
@@ -6195,7 +6136,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'${lucideIcons
|
|
|
6195
6136
|
import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
|
|
6196
6137
|
import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
|
|
6197
6138
|
import { toast } from 'sonner'
|
|
6198
|
-
import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
6139
|
+
import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
6199
6140
|
${relHookImports ? `${relHookImports}
|
|
6200
6141
|
` : ""}${uiImports.join("\n")}
|
|
6201
6142
|
import type {
|
|
@@ -6486,60 +6427,52 @@ function parseSingleItem2(str) {
|
|
|
6486
6427
|
const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
|
|
6487
6428
|
const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
|
|
6488
6429
|
const iconMatch = str.match(/icon:\s*(\w+)/);
|
|
6430
|
+
const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
|
|
6489
6431
|
if (!labelMatch || !hrefMatch) return null;
|
|
6490
6432
|
const item = {
|
|
6491
6433
|
label: labelMatch[1],
|
|
6492
6434
|
href: hrefMatch[1]
|
|
6493
6435
|
};
|
|
6494
6436
|
if (iconMatch) item.icon = iconMatch[1];
|
|
6495
|
-
|
|
6496
|
-
if (childrenMatch) {
|
|
6497
|
-
item.children = parseItemsBlock2(childrenMatch[1]);
|
|
6498
|
-
}
|
|
6437
|
+
if (groupMatch) item.group = groupMatch[1];
|
|
6499
6438
|
return item;
|
|
6500
6439
|
}
|
|
6501
6440
|
function generateNavigationCode2(items, iconImports) {
|
|
6502
6441
|
const lines = [];
|
|
6503
|
-
lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
|
|
6504
6442
|
lines.push("import type { LucideIcon } from 'lucide-react'");
|
|
6443
|
+
lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
|
|
6505
6444
|
lines.push("");
|
|
6506
6445
|
lines.push("export interface CmsNavigationItem {");
|
|
6507
6446
|
lines.push(" label: string");
|
|
6508
6447
|
lines.push(" href: string");
|
|
6509
6448
|
lines.push(" icon?: LucideIcon");
|
|
6510
|
-
lines.push("
|
|
6449
|
+
lines.push(" group?: string");
|
|
6511
6450
|
lines.push("}");
|
|
6512
6451
|
lines.push("");
|
|
6513
6452
|
lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
|
|
6514
6453
|
for (let i = 0; i < items.length; i++) {
|
|
6515
6454
|
const item = items[i];
|
|
6516
6455
|
const isLast = i === items.length - 1;
|
|
6517
|
-
appendItem2(lines, item,
|
|
6456
|
+
appendItem2(lines, item, isLast);
|
|
6518
6457
|
}
|
|
6519
6458
|
lines.push("]");
|
|
6520
6459
|
lines.push("");
|
|
6521
6460
|
return lines.join("\n");
|
|
6522
6461
|
}
|
|
6523
|
-
function appendItem2(lines, item,
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
lines.push(
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
lines.push(
|
|
6535
|
-
|
|
6536
|
-
}
|
|
6537
|
-
lines.push(`${pad}{`);
|
|
6538
|
-
lines.push(`${pad} label: '${item.label}',`);
|
|
6539
|
-
lines.push(`${pad} href: '${item.href}'${item.icon ? "," : ""}`);
|
|
6540
|
-
if (item.icon) lines.push(`${pad} icon: ${item.icon}`);
|
|
6541
|
-
lines.push(`${pad}}${isLast ? "" : ","}`);
|
|
6542
|
-
}
|
|
6462
|
+
function appendItem2(lines, item, isLast) {
|
|
6463
|
+
lines.push(" {");
|
|
6464
|
+
lines.push(` label: '${item.label}',`);
|
|
6465
|
+
const hasMore = item.icon != null || item.group != null;
|
|
6466
|
+
lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
|
|
6467
|
+
if (item.icon && item.group) {
|
|
6468
|
+
lines.push(` icon: ${item.icon},`);
|
|
6469
|
+
lines.push(` group: '${item.group}'`);
|
|
6470
|
+
} else if (item.icon) {
|
|
6471
|
+
lines.push(` icon: ${item.icon}`);
|
|
6472
|
+
} else if (item.group) {
|
|
6473
|
+
lines.push(` group: '${item.group}'`);
|
|
6474
|
+
}
|
|
6475
|
+
lines.push(` }${isLast ? "" : ","}`);
|
|
6543
6476
|
}
|
|
6544
6477
|
function updateNavigation(schema, cwd, cmsDir, options = {}) {
|
|
6545
6478
|
const navFilePath = path15.join(cwd, cmsDir, "data", "navigation.ts");
|
|
@@ -6561,48 +6494,26 @@ function updateNavigation(schema, cwd, cmsDir, options = {}) {
|
|
|
6561
6494
|
icon: schema.icon
|
|
6562
6495
|
};
|
|
6563
6496
|
if (schema.navGroup) {
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
children: []
|
|
6571
|
-
};
|
|
6572
|
-
items.push(group2);
|
|
6573
|
-
}
|
|
6574
|
-
if (!group2.children) {
|
|
6575
|
-
group2.children = [];
|
|
6576
|
-
}
|
|
6577
|
-
const existingChild = group2.children.findIndex((c) => c.href === entityHref);
|
|
6578
|
-
if (existingChild >= 0) {
|
|
6579
|
-
if (options.force) {
|
|
6580
|
-
group2.children[existingChild] = newItem;
|
|
6581
|
-
} else {
|
|
6582
|
-
return { files: [] };
|
|
6583
|
-
}
|
|
6497
|
+
newItem.group = schema.navGroup.label;
|
|
6498
|
+
}
|
|
6499
|
+
const existingIndex = items.findIndex((item) => item.href === entityHref);
|
|
6500
|
+
if (existingIndex >= 0) {
|
|
6501
|
+
if (options.force) {
|
|
6502
|
+
items[existingIndex] = newItem;
|
|
6584
6503
|
} else {
|
|
6585
|
-
|
|
6586
|
-
group2.children.sort((a, b) => a.label.localeCompare(b.label));
|
|
6587
|
-
}
|
|
6588
|
-
if (schema.navGroup.icon && !iconImports.includes(schema.navGroup.icon)) {
|
|
6589
|
-
iconImports.push(schema.navGroup.icon);
|
|
6504
|
+
return { files: [] };
|
|
6590
6505
|
}
|
|
6591
6506
|
} else {
|
|
6592
|
-
|
|
6593
|
-
if (existingIndex >= 0) {
|
|
6594
|
-
if (options.force) {
|
|
6595
|
-
items[existingIndex] = newItem;
|
|
6596
|
-
} else {
|
|
6597
|
-
return { files: [] };
|
|
6598
|
-
}
|
|
6599
|
-
} else {
|
|
6600
|
-
items.push(newItem);
|
|
6601
|
-
}
|
|
6507
|
+
items.push(newItem);
|
|
6602
6508
|
}
|
|
6603
6509
|
const dashboard = items.find((item) => item.href === "/cms");
|
|
6604
6510
|
const others = items.filter((item) => item.href !== "/cms");
|
|
6605
|
-
others.sort((a, b) =>
|
|
6511
|
+
others.sort((a, b) => {
|
|
6512
|
+
if (!a.group && b.group) return -1;
|
|
6513
|
+
if (a.group && !b.group) return 1;
|
|
6514
|
+
if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
|
|
6515
|
+
return a.label.localeCompare(b.label);
|
|
6516
|
+
});
|
|
6606
6517
|
items = [...dashboard ? [dashboard] : [], ...others];
|
|
6607
6518
|
if (schema.icon && !iconImports.includes(schema.icon)) {
|
|
6608
6519
|
iconImports.push(schema.icon);
|
|
@@ -6721,8 +6632,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
|
|
|
6721
6632
|
const hasCreate = schema.actions?.create ?? false;
|
|
6722
6633
|
const hasDelete = schema.actions?.delete ?? false;
|
|
6723
6634
|
const hasFilters = schema.filters && schema.filters.length > 0;
|
|
6724
|
-
const lucideIcons = ["Search"];
|
|
6725
|
-
if (hasCreate) lucideIcons.push("
|
|
6635
|
+
const lucideIcons = ["ChevronLeft", "Search", "CornerDownLeft"];
|
|
6636
|
+
if (hasCreate) lucideIcons.push("Plus");
|
|
6726
6637
|
if (hasDelete) lucideIcons.push("Trash2");
|
|
6727
6638
|
if (hasFilters) lucideIcons.push("Check", "ChevronsUpDown");
|
|
6728
6639
|
let imports = `'use client'
|
|
@@ -6730,7 +6641,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
|
|
|
6730
6641
|
import { useQueryClient } from '@tanstack/react-query'
|
|
6731
6642
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
6732
6643
|
import { ${lucideIcons.join(", ")} } from 'lucide-react'
|
|
6733
|
-
${hasCreate ? "import Link from 'next/link'\n" : ""}import {
|
|
6644
|
+
${hasCreate ? "import Link from 'next/link'\n" : ""}import { useRouter } from 'next/navigation'
|
|
6645
|
+
import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
|
|
6734
6646
|
import * as React from 'react'
|
|
6735
6647
|
import { useFormStatus } from 'react-dom'
|
|
6736
6648
|
${hasDelete ? "import { toast } from 'sonner'\n" : ""}import { PageHeader } from '@cms/components/shared/page-header'
|
|
@@ -6830,7 +6742,7 @@ ${filterLogic}` : ""}`;
|
|
|
6830
6742
|
})
|
|
6831
6743
|
}
|
|
6832
6744
|
` : "";
|
|
6833
|
-
const
|
|
6745
|
+
const _filterDropdowns = hasFilters ? schema.filters.map(
|
|
6834
6746
|
(f) => ` <Popover open={${f.field}ComboboxOpen} onOpenChange={set${toPascalCase14(f.field)}ComboboxOpen}>
|
|
6835
6747
|
<PopoverTrigger asChild>
|
|
6836
6748
|
<Button
|
|
@@ -6893,19 +6805,17 @@ ${filterLogic}` : ""}`;
|
|
|
6893
6805
|
</PopoverContent>
|
|
6894
6806
|
</Popover>`
|
|
6895
6807
|
).join("\n") : "";
|
|
6896
|
-
const searchInput =
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
<SearchButton />
|
|
6908
|
-
</form>`;
|
|
6808
|
+
const searchInput = `<form action={searchAction} className="flex items-center gap-2 relative">
|
|
6809
|
+
<Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
|
|
6810
|
+
<Input
|
|
6811
|
+
key={search}
|
|
6812
|
+
name="search"
|
|
6813
|
+
placeholder="Search ${schema.label.toLowerCase()}..."
|
|
6814
|
+
defaultValue={search}
|
|
6815
|
+
className="w-64 pl-9 bg-white rounded-lg"
|
|
6816
|
+
/>
|
|
6817
|
+
<CornerDownLeft className="text-muted-foreground/70 pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2" />
|
|
6818
|
+
</form>`;
|
|
6909
6819
|
const deleteButton = hasDelete ? ` {selectedIds.length > 0 && (
|
|
6910
6820
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
6911
6821
|
<AlertDialogTrigger asChild>
|
|
@@ -6941,7 +6851,7 @@ ${filterLogic}` : ""}`;
|
|
|
6941
6851
|
)}` : "";
|
|
6942
6852
|
const createButton = hasCreate ? ` <Button asChild>
|
|
6943
6853
|
<Link href="/cms/${schema.name}/new">
|
|
6944
|
-
<
|
|
6854
|
+
<Plus className="size-3.5 -ml-0.5" strokeWidth={2} />
|
|
6945
6855
|
Create ${singularizeLabel2(schema.label)}
|
|
6946
6856
|
</Link>
|
|
6947
6857
|
</Button>` : "";
|
|
@@ -6956,20 +6866,12 @@ ${searchButton}interface ${Plural}PageContentProps<TValue> {
|
|
|
6956
6866
|
export function ${Plural}PageContent<TValue>({
|
|
6957
6867
|
columns
|
|
6958
6868
|
}: ${Plural}PageContentProps<TValue>) {
|
|
6869
|
+
const router = useRouter()
|
|
6959
6870
|
const queryClient = useQueryClient()
|
|
6960
6871
|
${searchLogic}${deleteLogic}
|
|
6961
6872
|
return (
|
|
6962
6873
|
<>
|
|
6963
|
-
<div className="flex items-center
|
|
6964
|
-
<PageHeader title="${schema.label}" description="${schema.description}" />
|
|
6965
|
-
<div className="flex items-center gap-2">
|
|
6966
|
-
${filterDropdowns ? `${filterDropdowns}
|
|
6967
|
-
` : ""}${searchInput}
|
|
6968
|
-
${deleteButton}
|
|
6969
|
-
${createButton}
|
|
6970
|
-
</div>
|
|
6971
|
-
</div>
|
|
6972
|
-
|
|
6874
|
+
<PageHeader title="${schema.label}" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} search={${searchInput}} actions={<div className="flex items-center gap-2">${deleteButton} ${createButton}</div>} />
|
|
6973
6875
|
<main className="space-y-6 p-6">
|
|
6974
6876
|
<${Plural}Table ${tableProps} />
|
|
6975
6877
|
</main>
|
|
@@ -7018,14 +6920,12 @@ export default async function ${PageName}Page() {
|
|
|
7018
6920
|
const data = await get${Singular}()
|
|
7019
6921
|
|
|
7020
6922
|
return (
|
|
7021
|
-
|
|
7022
|
-
<
|
|
7023
|
-
|
|
7024
|
-
</div>
|
|
7025
|
-
<main className="container mx-auto max-w-5xl p-6">
|
|
6923
|
+
<>
|
|
6924
|
+
<PageHeader title="${schema.label}" />
|
|
6925
|
+
<main className="container mx-auto max-w-5xl p-6 pb-20">
|
|
7026
6926
|
<${Singular}Form initialData={data} />
|
|
7027
6927
|
</main>
|
|
7028
|
-
|
|
6928
|
+
</>
|
|
7029
6929
|
)
|
|
7030
6930
|
}
|
|
7031
6931
|
`;
|
|
@@ -8059,8 +7959,8 @@ export const { GET, POST } = toNextJsHandler(auth)
|
|
|
8059
7959
|
function uploadRouteTemplate() {
|
|
8060
7960
|
return `import { PutObjectCommand } from '@aws-sdk/client-s3'
|
|
8061
7961
|
import { BUCKET_NAME, generateFilePath, getPublicUrl, getR2Client } from '@cms/lib/r2'
|
|
8062
|
-
import { validateFiles } from '@cms/utils/validation'
|
|
8063
7962
|
import type { UploadedFile } from '@cms/types'
|
|
7963
|
+
import { validateFiles } from '@cms/utils/validation'
|
|
8064
7964
|
import { type NextRequest, NextResponse } from 'next/server'
|
|
8065
7965
|
|
|
8066
7966
|
export async function POST(request: NextRequest) {
|
|
@@ -8080,27 +7980,22 @@ export async function POST(request: NextRequest) {
|
|
|
8080
7980
|
}
|
|
8081
7981
|
|
|
8082
7982
|
if (files.length === 0) {
|
|
8083
|
-
return NextResponse.json(
|
|
8084
|
-
{ success: false, error: 'No files provided' },
|
|
8085
|
-
{ status: 400 }
|
|
8086
|
-
)
|
|
7983
|
+
return NextResponse.json({ success: false, error: 'No files provided' }, { status: 400 })
|
|
8087
7984
|
}
|
|
8088
7985
|
|
|
8089
7986
|
const validation = validateFiles(files, {
|
|
8090
7987
|
maxSizeInBytes,
|
|
8091
7988
|
allowedTypes,
|
|
8092
|
-
maxFiles: 10
|
|
7989
|
+
maxFiles: 10,
|
|
8093
7990
|
})
|
|
8094
7991
|
|
|
8095
7992
|
if (!validation.valid) {
|
|
8096
7993
|
return NextResponse.json(
|
|
8097
7994
|
{
|
|
8098
7995
|
success: false,
|
|
8099
|
-
error: validation.errors
|
|
8100
|
-
.map((e) => \`\${e.filename}: \${e.error}\`)
|
|
8101
|
-
.join('; ')
|
|
7996
|
+
error: validation.errors.map((e) => \`\${e.filename}: \${e.error}\`).join('; '),
|
|
8102
7997
|
},
|
|
8103
|
-
{ status: 400 }
|
|
7998
|
+
{ status: 400 },
|
|
8104
7999
|
)
|
|
8105
8000
|
}
|
|
8106
8001
|
|
|
@@ -8116,7 +8011,7 @@ export async function POST(request: NextRequest) {
|
|
|
8116
8011
|
Key: key,
|
|
8117
8012
|
Body: buffer,
|
|
8118
8013
|
ContentType: file.type,
|
|
8119
|
-
ContentLength: file.size
|
|
8014
|
+
ContentLength: file.size,
|
|
8120
8015
|
})
|
|
8121
8016
|
|
|
8122
8017
|
await getR2Client().send(command)
|
|
@@ -8126,18 +8021,14 @@ export async function POST(request: NextRequest) {
|
|
|
8126
8021
|
url: getPublicUrl(key),
|
|
8127
8022
|
filename: file.name,
|
|
8128
8023
|
size: file.size,
|
|
8129
|
-
contentType: file.type
|
|
8024
|
+
contentType: file.type,
|
|
8130
8025
|
})
|
|
8131
8026
|
}
|
|
8132
8027
|
|
|
8133
8028
|
return NextResponse.json({ success: true, files: uploadedFiles })
|
|
8134
8029
|
} catch (error) {
|
|
8135
|
-
const message =
|
|
8136
|
-
|
|
8137
|
-
return NextResponse.json(
|
|
8138
|
-
{ success: false, error: message },
|
|
8139
|
-
{ status: 500 }
|
|
8140
|
-
)
|
|
8030
|
+
const message = error instanceof Error ? error.message : 'Failed to upload files'
|
|
8031
|
+
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
|
8141
8032
|
}
|
|
8142
8033
|
}
|
|
8143
8034
|
`;
|
|
@@ -8217,7 +8108,9 @@ function authClientTemplate() {
|
|
|
8217
8108
|
import { createAuthClient } from 'better-auth/react'
|
|
8218
8109
|
|
|
8219
8110
|
export const authClient = createAuthClient({
|
|
8220
|
-
baseURL:
|
|
8111
|
+
baseURL:
|
|
8112
|
+
process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
|
|
8113
|
+
(typeof window !== 'undefined' ? window.location.origin : ''),
|
|
8221
8114
|
basePath: '/api/cms/auth',
|
|
8222
8115
|
})
|
|
8223
8116
|
|
|
@@ -8238,10 +8131,7 @@ export enum UserRole {
|
|
|
8238
8131
|
}
|
|
8239
8132
|
|
|
8240
8133
|
export function isUserRole(value: unknown): value is UserRole {
|
|
8241
|
-
return (
|
|
8242
|
-
typeof value === 'string' &&
|
|
8243
|
-
Object.values(UserRole).includes(value as UserRole)
|
|
8244
|
-
)
|
|
8134
|
+
return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
|
|
8245
8135
|
}
|
|
8246
8136
|
|
|
8247
8137
|
/**
|
|
@@ -8696,184 +8586,121 @@ function hasEnvBetterstartVars(cwd) {
|
|
|
8696
8586
|
// src/init/templates/components/cms-globals.ts
|
|
8697
8587
|
function cmsGlobalsCssTemplate() {
|
|
8698
8588
|
return `@import "tailwindcss";
|
|
8589
|
+
@import "tw-animate-css";
|
|
8590
|
+
@import "shadcn/tailwind.css";
|
|
8699
8591
|
|
|
8700
8592
|
@custom-variant dark (&:is(.dark *));
|
|
8701
8593
|
|
|
8702
|
-
:root {
|
|
8703
|
-
--background: oklch(0.985 0 0);
|
|
8704
|
-
--foreground: oklch(0 0 0);
|
|
8705
|
-
--card: oklch(1 0 0);
|
|
8706
|
-
--card-foreground: oklch(0 0 0);
|
|
8707
|
-
--popover: oklch(0.99 0 0);
|
|
8708
|
-
--popover-foreground: oklch(0 0 0);
|
|
8709
|
-
--primary: oklch(0 0 0);
|
|
8710
|
-
--primary-foreground: oklch(1 0 0);
|
|
8711
|
-
--secondary: oklch(0.97 0 0);
|
|
8712
|
-
--secondary-foreground: oklch(0 0 0);
|
|
8713
|
-
--muted: oklch(0.97 0 0);
|
|
8714
|
-
--muted-foreground: oklch(0.44 0 0);
|
|
8715
|
-
--accent: oklch(0.97 0 0);
|
|
8716
|
-
--accent-foreground: oklch(0 0 0);
|
|
8717
|
-
--destructive: oklch(0.63 0.19 23.03);
|
|
8718
|
-
--destructive-foreground: oklch(1 0 0);
|
|
8719
|
-
--border: oklch(0.92 0 0);
|
|
8720
|
-
--input: oklch(0.99 0 0);
|
|
8721
|
-
--ring: oklch(0 0 0);
|
|
8722
|
-
--chart-1: oklch(0.81 0.17 75.35);
|
|
8723
|
-
--chart-2: oklch(0.55 0.22 264.53);
|
|
8724
|
-
--chart-3: oklch(0.72 0 0);
|
|
8725
|
-
--chart-4: oklch(0.92 0 0);
|
|
8726
|
-
--chart-5: oklch(0.56 0 0);
|
|
8727
|
-
--sidebar: oklch(1 0 0);
|
|
8728
|
-
--sidebar-foreground: oklch(0 0 0);
|
|
8729
|
-
--sidebar-primary: oklch(0 0 0);
|
|
8730
|
-
--sidebar-primary-foreground: oklch(1 0 0);
|
|
8731
|
-
--sidebar-accent: oklch(0.94 0 0);
|
|
8732
|
-
--sidebar-accent-foreground: oklch(0 0 0);
|
|
8733
|
-
--sidebar-border: oklch(0.94 0 0);
|
|
8734
|
-
--sidebar-ring: oklch(0 0 0);
|
|
8735
|
-
--font-sans: Geist, sans-serif;
|
|
8736
|
-
--font-serif: Georgia, serif;
|
|
8737
|
-
--font-mono: Geist Mono, monospace;
|
|
8738
|
-
--radius: 0.4rem;
|
|
8739
|
-
--shadow-x: 0px;
|
|
8740
|
-
--shadow-y: 1px;
|
|
8741
|
-
--shadow-blur: 3px;
|
|
8742
|
-
--shadow-spread: 0px;
|
|
8743
|
-
--shadow-opacity: 0.02;
|
|
8744
|
-
--shadow-color: hsl(0 0% 0%);
|
|
8745
|
-
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8746
|
-
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8747
|
-
--shadow-sm:
|
|
8748
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8749
|
-
--shadow:
|
|
8750
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8751
|
-
--shadow-md:
|
|
8752
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
|
|
8753
|
-
--shadow-lg:
|
|
8754
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
|
|
8755
|
-
--shadow-xl:
|
|
8756
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
|
|
8757
|
-
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
|
|
8758
|
-
--tracking-normal: 0em;
|
|
8759
|
-
--spacing: 0.25rem;
|
|
8760
|
-
}
|
|
8761
|
-
|
|
8762
|
-
.dark {
|
|
8763
|
-
--background: oklch(0 0 0);
|
|
8764
|
-
--foreground: oklch(1 0 0);
|
|
8765
|
-
--card: oklch(0.14 0 0);
|
|
8766
|
-
--card-foreground: oklch(1 0 0);
|
|
8767
|
-
--popover: oklch(0.18 0 0);
|
|
8768
|
-
--popover-foreground: oklch(1 0 0);
|
|
8769
|
-
--primary: oklch(1 0 0);
|
|
8770
|
-
--primary-foreground: oklch(0 0 0);
|
|
8771
|
-
--secondary: oklch(0.25 0 0);
|
|
8772
|
-
--secondary-foreground: oklch(1 0 0);
|
|
8773
|
-
--muted: oklch(0.23 0 0);
|
|
8774
|
-
--muted-foreground: oklch(0.72 0 0);
|
|
8775
|
-
--accent: oklch(0.32 0 0);
|
|
8776
|
-
--accent-foreground: oklch(1 0 0);
|
|
8777
|
-
--destructive: oklch(0.69 0.2 23.91);
|
|
8778
|
-
--destructive-foreground: oklch(0 0 0);
|
|
8779
|
-
--border: oklch(0.26 0 0);
|
|
8780
|
-
--input: oklch(0.32 0 0);
|
|
8781
|
-
--ring: oklch(0.72 0 0);
|
|
8782
|
-
--chart-1: oklch(0.81 0.17 75.35);
|
|
8783
|
-
--chart-2: oklch(0.58 0.21 260.84);
|
|
8784
|
-
--chart-3: oklch(0.56 0 0);
|
|
8785
|
-
--chart-4: oklch(0.44 0 0);
|
|
8786
|
-
--chart-5: oklch(0.92 0 0);
|
|
8787
|
-
--sidebar: oklch(0.18 0 0);
|
|
8788
|
-
--sidebar-foreground: oklch(1 0 0);
|
|
8789
|
-
--sidebar-primary: oklch(1 0 0);
|
|
8790
|
-
--sidebar-primary-foreground: oklch(0 0 0);
|
|
8791
|
-
--sidebar-accent: oklch(0.32 0 0);
|
|
8792
|
-
--sidebar-accent-foreground: oklch(1 0 0);
|
|
8793
|
-
--sidebar-border: oklch(0.32 0 0);
|
|
8794
|
-
--sidebar-ring: oklch(0.72 0 0);
|
|
8795
|
-
--font-sans: Geist, sans-serif;
|
|
8796
|
-
--font-serif: Georgia, serif;
|
|
8797
|
-
--font-mono: Geist Mono, monospace;
|
|
8798
|
-
--radius: 0.4rem;
|
|
8799
|
-
--shadow-x: 0px;
|
|
8800
|
-
--shadow-y: 1px;
|
|
8801
|
-
--shadow-blur: 3px;
|
|
8802
|
-
--shadow-spread: 0px;
|
|
8803
|
-
--shadow-opacity: 0.02;
|
|
8804
|
-
--shadow-color: hsl(0 0% 0%);
|
|
8805
|
-
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8806
|
-
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8807
|
-
--shadow-sm:
|
|
8808
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8809
|
-
--shadow:
|
|
8810
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8811
|
-
--shadow-md:
|
|
8812
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
|
|
8813
|
-
--shadow-lg:
|
|
8814
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
|
|
8815
|
-
--shadow-xl:
|
|
8816
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
|
|
8817
|
-
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
|
|
8818
|
-
}
|
|
8819
|
-
|
|
8820
|
-
.cms-root {
|
|
8821
|
-
--font-sans: var(--font-geist-sans, sans-serif);
|
|
8822
|
-
--font-mono: var(--font-geist-mono, monospace);
|
|
8823
|
-
font-family: var(--font-sans);
|
|
8824
|
-
}
|
|
8825
|
-
|
|
8826
8594
|
@theme inline {
|
|
8827
8595
|
--color-background: var(--background);
|
|
8828
8596
|
--color-foreground: var(--foreground);
|
|
8829
|
-
--color-card: var(--card);
|
|
8830
|
-
--color-card-foreground: var(--card-foreground);
|
|
8831
|
-
--color-popover: var(--popover);
|
|
8832
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
8833
|
-
--color-primary: var(--primary);
|
|
8834
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
8835
|
-
--color-secondary: var(--secondary);
|
|
8836
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
8837
|
-
--color-muted: var(--muted);
|
|
8838
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
8839
|
-
--color-accent: var(--accent);
|
|
8840
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
8841
|
-
--color-destructive: var(--destructive);
|
|
8842
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
8843
|
-
--color-border: var(--border);
|
|
8844
|
-
--color-input: var(--input);
|
|
8845
|
-
--color-ring: var(--ring);
|
|
8846
|
-
--color-chart-1: var(--chart-1);
|
|
8847
|
-
--color-chart-2: var(--chart-2);
|
|
8848
|
-
--color-chart-3: var(--chart-3);
|
|
8849
|
-
--color-chart-4: var(--chart-4);
|
|
8850
|
-
--color-chart-5: var(--chart-5);
|
|
8851
|
-
--color-sidebar: var(--sidebar);
|
|
8852
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
8853
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
8854
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
8855
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
8856
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
8857
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
8858
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
8859
|
-
|
|
8860
8597
|
--font-sans: var(--font-sans);
|
|
8861
|
-
--font-mono: var(--font-mono);
|
|
8862
|
-
--
|
|
8863
|
-
|
|
8598
|
+
--font-mono: var(--font-geist-mono);
|
|
8599
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
8600
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
8601
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
8602
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
8603
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
8604
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
8605
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
8606
|
+
--color-sidebar: var(--sidebar);
|
|
8607
|
+
--color-chart-5: var(--chart-5);
|
|
8608
|
+
--color-chart-4: var(--chart-4);
|
|
8609
|
+
--color-chart-3: var(--chart-3);
|
|
8610
|
+
--color-chart-2: var(--chart-2);
|
|
8611
|
+
--color-chart-1: var(--chart-1);
|
|
8612
|
+
--color-ring: var(--ring);
|
|
8613
|
+
--color-input: var(--input);
|
|
8614
|
+
--color-border: var(--border);
|
|
8615
|
+
--color-destructive: var(--destructive);
|
|
8616
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
8617
|
+
--color-accent: var(--accent);
|
|
8618
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
8619
|
+
--color-muted: var(--muted);
|
|
8620
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
8621
|
+
--color-secondary: var(--secondary);
|
|
8622
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
8623
|
+
--color-primary: var(--primary);
|
|
8624
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
8625
|
+
--color-popover: var(--popover);
|
|
8626
|
+
--color-card-foreground: var(--card-foreground);
|
|
8627
|
+
--color-card: var(--card);
|
|
8864
8628
|
--radius-sm: calc(var(--radius) - 4px);
|
|
8865
8629
|
--radius-md: calc(var(--radius) - 2px);
|
|
8866
8630
|
--radius-lg: var(--radius);
|
|
8867
8631
|
--radius-xl: calc(var(--radius) + 4px);
|
|
8632
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
8633
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
8634
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
8635
|
+
}
|
|
8636
|
+
|
|
8637
|
+
:root {
|
|
8638
|
+
--background: oklch(1 0 0);
|
|
8639
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
8640
|
+
--card: oklch(1 0 0);
|
|
8641
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
8642
|
+
--popover: oklch(1 0 0);
|
|
8643
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
8644
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
8645
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
8646
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
8647
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
8648
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
8649
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
8650
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
8651
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
8652
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
8653
|
+
--border: oklch(0.92 0.004 286.32);
|
|
8654
|
+
--input: oklch(0.92 0.004 286.32);
|
|
8655
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
8656
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
8657
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
8658
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
8659
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
8660
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
8661
|
+
--radius: 0.625rem;
|
|
8662
|
+
--sidebar: oklch(0.985 0 0);
|
|
8663
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
8664
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
8665
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
8666
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
8667
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
8668
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
8669
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
8670
|
+
}
|
|
8868
8671
|
|
|
8869
|
-
|
|
8870
|
-
--
|
|
8871
|
-
--
|
|
8872
|
-
--
|
|
8873
|
-
--
|
|
8874
|
-
--
|
|
8875
|
-
--
|
|
8876
|
-
--
|
|
8672
|
+
.dark {
|
|
8673
|
+
--background: oklch(0.141 0.005 285.823);
|
|
8674
|
+
--foreground: oklch(0.985 0 0);
|
|
8675
|
+
--card: oklch(0.21 0.006 285.885);
|
|
8676
|
+
--card-foreground: oklch(0.985 0 0);
|
|
8677
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
8678
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
8679
|
+
--primary: oklch(0.92 0.004 286.32);
|
|
8680
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
8681
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
8682
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
8683
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
8684
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
8685
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
8686
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
8687
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
8688
|
+
--border: oklch(1 0 0 / 10%);
|
|
8689
|
+
--input: oklch(1 0 0 / 15%);
|
|
8690
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
8691
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
8692
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
8693
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
8694
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
8695
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
8696
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
8697
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
8698
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
8699
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
8700
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
8701
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
8702
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
8703
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
8877
8704
|
}
|
|
8878
8705
|
|
|
8879
8706
|
@layer base {
|
|
@@ -8881,7 +8708,7 @@ function cmsGlobalsCssTemplate() {
|
|
|
8881
8708
|
@apply border-border outline-ring/50;
|
|
8882
8709
|
}
|
|
8883
8710
|
body {
|
|
8884
|
-
@apply bg-background text-foreground
|
|
8711
|
+
@apply bg-background text-foreground;
|
|
8885
8712
|
}
|
|
8886
8713
|
}
|
|
8887
8714
|
|
|
@@ -8914,6 +8741,14 @@ function cmsGlobalsCssTemplate() {
|
|
|
8914
8741
|
function dataTableTemplate() {
|
|
8915
8742
|
return `'use client'
|
|
8916
8743
|
|
|
8744
|
+
import {
|
|
8745
|
+
Table,
|
|
8746
|
+
TableBody,
|
|
8747
|
+
TableCell,
|
|
8748
|
+
TableHead,
|
|
8749
|
+
TableHeader,
|
|
8750
|
+
TableRow,
|
|
8751
|
+
} from '@cms/components/ui/table'
|
|
8917
8752
|
import {
|
|
8918
8753
|
type ColumnDef,
|
|
8919
8754
|
type ColumnFiltersState,
|
|
@@ -8923,20 +8758,11 @@ import {
|
|
|
8923
8758
|
getPaginationRowModel,
|
|
8924
8759
|
getSortedRowModel,
|
|
8925
8760
|
type SortingState,
|
|
8926
|
-
type Table as TanstackTable,
|
|
8927
8761
|
useReactTable,
|
|
8928
|
-
type VisibilityState
|
|
8762
|
+
type VisibilityState,
|
|
8929
8763
|
} from '@tanstack/react-table'
|
|
8930
8764
|
import { parseAsInteger, useQueryState } from 'nuqs'
|
|
8931
8765
|
import * as React from 'react'
|
|
8932
|
-
import {
|
|
8933
|
-
Table,
|
|
8934
|
-
TableBody,
|
|
8935
|
-
TableCell,
|
|
8936
|
-
TableHead,
|
|
8937
|
-
TableHeader,
|
|
8938
|
-
TableRow
|
|
8939
|
-
} from '@cms/components/ui/table'
|
|
8940
8766
|
import { DataTablePagination } from './data-table-pagination'
|
|
8941
8767
|
|
|
8942
8768
|
interface DataTableProps<TData, TValue> {
|
|
@@ -8962,7 +8788,7 @@ export function DataTable<TData, TValue>({
|
|
|
8962
8788
|
selectedIds,
|
|
8963
8789
|
onSelectedIdsChange,
|
|
8964
8790
|
meta,
|
|
8965
|
-
getId = (row) => (row as { id: number }).id
|
|
8791
|
+
getId = (row) => (row as { id: number }).id,
|
|
8966
8792
|
}: DataTableProps<TData, TValue>) {
|
|
8967
8793
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
8968
8794
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
|
@@ -8985,27 +8811,38 @@ export function DataTable<TData, TValue>({
|
|
|
8985
8811
|
}, [selectedIds, data, getId])
|
|
8986
8812
|
|
|
8987
8813
|
const handleRowSelectionChange = React.useCallback(
|
|
8988
|
-
(
|
|
8814
|
+
(
|
|
8815
|
+
updater:
|
|
8816
|
+
| Record<string, boolean>
|
|
8817
|
+
| ((old: Record<string, boolean>) => Record<string, boolean>),
|
|
8818
|
+
) => {
|
|
8989
8819
|
if (!onSelectedIdsChange) return
|
|
8990
8820
|
const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater
|
|
8991
8821
|
const newIds = Object.keys(newSelection)
|
|
8992
8822
|
.filter((key) => newSelection[key])
|
|
8993
|
-
.map((key) => getId(data[Number.parseInt(key)]))
|
|
8823
|
+
.map((key) => getId(data[Number.parseInt(key, 10)]))
|
|
8994
8824
|
.filter(Boolean)
|
|
8995
8825
|
onSelectedIdsChange(newIds)
|
|
8996
8826
|
},
|
|
8997
|
-
[data, rowSelection, onSelectedIdsChange, getId]
|
|
8827
|
+
[data, rowSelection, onSelectedIdsChange, getId],
|
|
8998
8828
|
)
|
|
8999
8829
|
|
|
9000
8830
|
const handlePaginationChange = React.useCallback(
|
|
9001
|
-
(
|
|
8831
|
+
(
|
|
8832
|
+
updater:
|
|
8833
|
+
| { pageIndex: number; pageSize: number }
|
|
8834
|
+
| ((old: { pageIndex: number; pageSize: number }) => {
|
|
8835
|
+
pageIndex: number
|
|
8836
|
+
pageSize: number
|
|
8837
|
+
}),
|
|
8838
|
+
) => {
|
|
9002
8839
|
const current = { pageIndex, pageSize: effectivePageSize }
|
|
9003
8840
|
const next = typeof updater === 'function' ? updater(current) : updater
|
|
9004
8841
|
React.startTransition(() => {
|
|
9005
8842
|
setPageIndex(next.pageIndex)
|
|
9006
8843
|
})
|
|
9007
8844
|
},
|
|
9008
|
-
[pageIndex, effectivePageSize, setPageIndex]
|
|
8845
|
+
[pageIndex, effectivePageSize, setPageIndex],
|
|
9009
8846
|
)
|
|
9010
8847
|
|
|
9011
8848
|
const table = useReactTable({
|
|
@@ -9028,9 +8865,9 @@ export function DataTable<TData, TValue>({
|
|
|
9028
8865
|
rowSelection,
|
|
9029
8866
|
pagination: {
|
|
9030
8867
|
pageIndex,
|
|
9031
|
-
pageSize: effectivePageSize
|
|
9032
|
-
}
|
|
9033
|
-
}
|
|
8868
|
+
pageSize: effectivePageSize,
|
|
8869
|
+
},
|
|
8870
|
+
},
|
|
9034
8871
|
})
|
|
9035
8872
|
|
|
9036
8873
|
return (
|
|
@@ -9097,23 +8934,23 @@ export type { DataTableProps }
|
|
|
9097
8934
|
function dataTablePaginationTemplate() {
|
|
9098
8935
|
return `'use client'
|
|
9099
8936
|
|
|
9100
|
-
import type { Table } from '@tanstack/react-table'
|
|
9101
|
-
import * as React from 'react'
|
|
9102
8937
|
import { Button } from '@cms/components/ui/button'
|
|
9103
8938
|
import {
|
|
9104
8939
|
Select,
|
|
9105
8940
|
SelectContent,
|
|
9106
8941
|
SelectItem,
|
|
9107
8942
|
SelectTrigger,
|
|
9108
|
-
SelectValue
|
|
8943
|
+
SelectValue,
|
|
9109
8944
|
} from '@cms/components/ui/select'
|
|
8945
|
+
import type { Table } from '@tanstack/react-table'
|
|
8946
|
+
import * as React from 'react'
|
|
9110
8947
|
|
|
9111
8948
|
const PAGE_SIZE_OPTIONS = [
|
|
9112
8949
|
{ value: '10', label: '10' },
|
|
9113
8950
|
{ value: '20', label: '20' },
|
|
9114
8951
|
{ value: '50', label: '50' },
|
|
9115
8952
|
{ value: '100', label: '100' },
|
|
9116
|
-
{ value: 'all', label: 'All' }
|
|
8953
|
+
{ value: 'all', label: 'All' },
|
|
9117
8954
|
]
|
|
9118
8955
|
|
|
9119
8956
|
interface DataTablePaginationProps<TData> {
|
|
@@ -9125,7 +8962,7 @@ interface DataTablePaginationProps<TData> {
|
|
|
9125
8962
|
export function DataTablePagination<TData>({
|
|
9126
8963
|
table,
|
|
9127
8964
|
pageSize,
|
|
9128
|
-
setPageSize
|
|
8965
|
+
setPageSize,
|
|
9129
8966
|
}: DataTablePaginationProps<TData>) {
|
|
9130
8967
|
const handlePageSizeChange = React.useCallback(
|
|
9131
8968
|
(value: string) => {
|
|
@@ -9138,7 +8975,7 @@ export function DataTablePagination<TData>({
|
|
|
9138
8975
|
table.setPageIndex(0)
|
|
9139
8976
|
})
|
|
9140
8977
|
},
|
|
9141
|
-
[setPageSize, table]
|
|
8978
|
+
[setPageSize, table],
|
|
9142
8979
|
)
|
|
9143
8980
|
|
|
9144
8981
|
return (
|
|
@@ -9192,11 +9029,11 @@ export function DataTablePagination<TData>({
|
|
|
9192
9029
|
function dataTableToolbarTemplate() {
|
|
9193
9030
|
return `'use client'
|
|
9194
9031
|
|
|
9195
|
-
import { ArrowUpDown, Save, Search } from 'lucide-react'
|
|
9196
|
-
import * as React from 'react'
|
|
9197
|
-
import { useFormStatus } from 'react-dom'
|
|
9198
9032
|
import { Button } from '@cms/components/ui/button'
|
|
9199
9033
|
import { Input } from '@cms/components/ui/input'
|
|
9034
|
+
import { ArrowUpDown, Save, Search } from 'lucide-react'
|
|
9035
|
+
import type * as React from 'react'
|
|
9036
|
+
import { useFormStatus } from 'react-dom'
|
|
9200
9037
|
|
|
9201
9038
|
function SearchButton() {
|
|
9202
9039
|
const { pending } = useFormStatus()
|
|
@@ -9218,7 +9055,7 @@ export function DataTableToolbar({
|
|
|
9218
9055
|
search,
|
|
9219
9056
|
onSearch,
|
|
9220
9057
|
searchPlaceholder = 'Search...',
|
|
9221
|
-
children
|
|
9058
|
+
children,
|
|
9222
9059
|
}: DataTableToolbarProps) {
|
|
9223
9060
|
return (
|
|
9224
9061
|
<div className="flex items-center gap-2">
|
|
@@ -9255,7 +9092,7 @@ export function ReorderControls({
|
|
|
9255
9092
|
onSave,
|
|
9256
9093
|
onCancel,
|
|
9257
9094
|
hasChanges,
|
|
9258
|
-
isSaving
|
|
9095
|
+
isSaving,
|
|
9259
9096
|
}: ReorderControlsProps) {
|
|
9260
9097
|
return (
|
|
9261
9098
|
<div className="flex items-center gap-2">
|
|
@@ -9270,26 +9107,14 @@ export function ReorderControls({
|
|
|
9270
9107
|
</Button>
|
|
9271
9108
|
{reorderMode && (
|
|
9272
9109
|
<>
|
|
9273
|
-
<Button
|
|
9274
|
-
variant="default"
|
|
9275
|
-
size="sm"
|
|
9276
|
-
onClick={onSave}
|
|
9277
|
-
disabled={!hasChanges || isSaving}
|
|
9278
|
-
>
|
|
9110
|
+
<Button variant="default" size="sm" onClick={onSave} disabled={!hasChanges || isSaving}>
|
|
9279
9111
|
<Save className="size-4 mr-1" />
|
|
9280
9112
|
{isSaving ? 'Saving...' : 'Save'}
|
|
9281
9113
|
</Button>
|
|
9282
|
-
<Button
|
|
9283
|
-
variant="outline"
|
|
9284
|
-
size="sm"
|
|
9285
|
-
onClick={onCancel}
|
|
9286
|
-
disabled={isSaving}
|
|
9287
|
-
>
|
|
9114
|
+
<Button variant="outline" size="sm" onClick={onCancel} disabled={isSaving}>
|
|
9288
9115
|
Cancel
|
|
9289
9116
|
</Button>
|
|
9290
|
-
{hasChanges &&
|
|
9291
|
-
<span className="text-sm text-muted-foreground">Unsaved changes</span>
|
|
9292
|
-
)}
|
|
9117
|
+
{hasChanges && <span className="text-sm text-muted-foreground">Unsaved changes</span>}
|
|
9293
9118
|
</>
|
|
9294
9119
|
)}
|
|
9295
9120
|
</div>
|
|
@@ -9302,7 +9127,6 @@ export function ReorderControls({
|
|
|
9302
9127
|
function cmsHeaderTemplate() {
|
|
9303
9128
|
return `'use client'
|
|
9304
9129
|
|
|
9305
|
-
import { CmsSearch } from '@cms/components/layout/cms-search'
|
|
9306
9130
|
import { Button } from '@cms/components/ui/button'
|
|
9307
9131
|
import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
|
|
9308
9132
|
import { useTheme } from '@cms/hooks/use-cms-theme'
|
|
@@ -9313,11 +9137,10 @@ export function CmsHeader() {
|
|
|
9313
9137
|
const { state } = useSidebar()
|
|
9314
9138
|
|
|
9315
9139
|
return (
|
|
9316
|
-
<header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50
|
|
9140
|
+
<header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50">
|
|
9317
9141
|
<div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
|
|
9318
9142
|
<div className="flex items-center gap-2 w-full">
|
|
9319
9143
|
{state === 'collapsed' && <SidebarTrigger />}
|
|
9320
|
-
<CmsSearch />
|
|
9321
9144
|
</div>
|
|
9322
9145
|
<div className="flex items-center gap-2 ml-auto">
|
|
9323
9146
|
<Button
|
|
@@ -9337,15 +9160,45 @@ export function CmsHeader() {
|
|
|
9337
9160
|
`;
|
|
9338
9161
|
}
|
|
9339
9162
|
|
|
9340
|
-
// src/init/templates/components/layout/cms-
|
|
9341
|
-
function
|
|
9163
|
+
// src/init/templates/components/layout/cms-nav-link.ts
|
|
9164
|
+
function cmsNavLinkTemplate() {
|
|
9165
|
+
return `'use client'
|
|
9166
|
+
|
|
9167
|
+
import { SidebarMenuButton } from '@cms/components/ui/sidebar'
|
|
9168
|
+
import Link from 'next/link'
|
|
9169
|
+
import { usePathname } from 'next/navigation'
|
|
9170
|
+
|
|
9171
|
+
export function CmsNavLink({
|
|
9172
|
+
href,
|
|
9173
|
+
children,
|
|
9174
|
+
}: {
|
|
9175
|
+
href: string
|
|
9176
|
+
children: React.ReactNode
|
|
9177
|
+
}) {
|
|
9178
|
+
const pathname = usePathname()
|
|
9179
|
+
const isActive =
|
|
9180
|
+
href === '/cms'
|
|
9181
|
+
? pathname === '/cms'
|
|
9182
|
+
: pathname === href || pathname.startsWith(href + '/')
|
|
9183
|
+
|
|
9184
|
+
return (
|
|
9185
|
+
<SidebarMenuButton asChild isActive={isActive}>
|
|
9186
|
+
<Link href={href}>{children}</Link>
|
|
9187
|
+
</SidebarMenuButton>
|
|
9188
|
+
)
|
|
9189
|
+
}
|
|
9190
|
+
`;
|
|
9191
|
+
}
|
|
9192
|
+
|
|
9193
|
+
// src/init/templates/components/layout/cms-providers.ts
|
|
9194
|
+
function cmsProvidersTemplate() {
|
|
9342
9195
|
return `'use client'
|
|
9343
9196
|
|
|
9344
|
-
import { useState } from 'react'
|
|
9345
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
9346
|
-
import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
|
|
9347
9197
|
import { Toaster } from '@cms/components/ui/sonner'
|
|
9198
|
+
import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
|
|
9199
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
9348
9200
|
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
|
9201
|
+
import { useState } from 'react'
|
|
9349
9202
|
|
|
9350
9203
|
export function CmsProviders({ children }: { children: React.ReactNode }) {
|
|
9351
9204
|
const [queryClient] = useState(
|
|
@@ -9385,15 +9238,15 @@ export const CmsSearch = () => {
|
|
|
9385
9238
|
<div className="flex items-center gap-2 relative w-full max-w-[240px]">
|
|
9386
9239
|
<Button
|
|
9387
9240
|
variant="outline"
|
|
9388
|
-
className="w-full text-left items-center pr-1! rounded-
|
|
9389
|
-
size="
|
|
9241
|
+
className="w-full text-left items-center pr-1.5! py-0 rounded-lg bg-white"
|
|
9242
|
+
size="lg"
|
|
9390
9243
|
>
|
|
9391
|
-
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground"
|
|
9392
|
-
<span className="w-full font-
|
|
9393
|
-
|
|
9244
|
+
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground/70" />
|
|
9245
|
+
<span className="w-full font-normal text-sm text-muted-foreground/70 [text-box-trim:trim-both]">
|
|
9246
|
+
Quick search...
|
|
9394
9247
|
</span>
|
|
9395
9248
|
<div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
|
|
9396
|
-
<Command className="size-3! text-muted-foreground"
|
|
9249
|
+
<Command className="size-3! text-muted-foreground" />
|
|
9397
9250
|
<span className="font-mono text-xs font-medium">K</span>
|
|
9398
9251
|
</div>
|
|
9399
9252
|
</Button>
|
|
@@ -9407,123 +9260,110 @@ CmsSearch.displayName = 'CmsSearch'
|
|
|
9407
9260
|
|
|
9408
9261
|
// src/init/templates/components/layout/cms-sidebar.ts
|
|
9409
9262
|
function cmsSidebarTemplate() {
|
|
9410
|
-
return `import {
|
|
9411
|
-
import { getSession } from '@cms/auth/middleware'
|
|
9263
|
+
return `import { getSession } from '@cms/auth/middleware'
|
|
9412
9264
|
import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
|
|
9413
|
-
import {
|
|
9414
|
-
Collapsible,
|
|
9415
|
-
CollapsibleContent,
|
|
9416
|
-
CollapsibleTrigger,
|
|
9417
|
-
} from '@cms/components/ui/collapsible'
|
|
9418
9265
|
import {
|
|
9419
9266
|
Sidebar,
|
|
9420
9267
|
SidebarContent,
|
|
9421
9268
|
SidebarFooter,
|
|
9269
|
+
SidebarGroup,
|
|
9270
|
+
SidebarGroupLabel,
|
|
9422
9271
|
SidebarHeader,
|
|
9423
9272
|
SidebarMenu,
|
|
9424
|
-
SidebarMenuButton,
|
|
9425
9273
|
SidebarMenuItem,
|
|
9426
|
-
SidebarMenuSub,
|
|
9427
|
-
SidebarMenuSubButton,
|
|
9428
|
-
SidebarMenuSubItem,
|
|
9429
|
-
SidebarRail,
|
|
9430
|
-
SidebarTrigger,
|
|
9431
9274
|
} from '@cms/components/ui/sidebar'
|
|
9432
9275
|
import { cms } from '@cms/data/cms'
|
|
9433
9276
|
import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
|
|
9434
|
-
import {
|
|
9277
|
+
import { Settings, Users } from 'lucide-react'
|
|
9435
9278
|
import Link from 'next/link'
|
|
9279
|
+
import { Fragment } from 'react'
|
|
9280
|
+
import { getSetting } from '@/cms/lib/actions/settings'
|
|
9281
|
+
import { CmsNavLink } from './cms-nav-link'
|
|
9282
|
+
import { CmsSearch } from './cms-search'
|
|
9436
9283
|
|
|
9437
|
-
function
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
<CollapsibleContent>
|
|
9450
|
-
<SidebarMenuSub>
|
|
9451
|
-
{item.children.map((child) => (
|
|
9452
|
-
<SidebarMenuSubItem key={child.href}>
|
|
9453
|
-
<SidebarMenuSubButton asChild>
|
|
9454
|
-
<Link href={child.href}>
|
|
9455
|
-
{child.icon && <child.icon className="size-3.5!" />}
|
|
9456
|
-
<span>{child.label}</span>
|
|
9457
|
-
</Link>
|
|
9458
|
-
</SidebarMenuSubButton>
|
|
9459
|
-
</SidebarMenuSubItem>
|
|
9460
|
-
))}
|
|
9461
|
-
</SidebarMenuSub>
|
|
9462
|
-
</CollapsibleContent>
|
|
9463
|
-
</SidebarMenuItem>
|
|
9464
|
-
</Collapsible>
|
|
9465
|
-
)
|
|
9284
|
+
function groupNavItems(items: CmsNavigationItem[]) {
|
|
9285
|
+
const groups: { label: string | null; items: CmsNavigationItem[] }[] = []
|
|
9286
|
+
const groupMap = new Map<string | null, CmsNavigationItem[]>()
|
|
9287
|
+
|
|
9288
|
+
for (const item of items) {
|
|
9289
|
+
const key = item.group ?? null
|
|
9290
|
+
if (!groupMap.has(key)) {
|
|
9291
|
+
const arr: CmsNavigationItem[] = []
|
|
9292
|
+
groupMap.set(key, arr)
|
|
9293
|
+
groups.push({ label: key, items: arr })
|
|
9294
|
+
}
|
|
9295
|
+
groupMap.get(key)!.push(item)
|
|
9466
9296
|
}
|
|
9467
9297
|
|
|
9468
|
-
return
|
|
9469
|
-
<SidebarMenuItem className="px-2">
|
|
9470
|
-
<SidebarMenuButton asChild>
|
|
9471
|
-
<Link href={item.href}>
|
|
9472
|
-
{item.icon && <item.icon className="size-3.5!" />}
|
|
9473
|
-
<span>{item.label}</span>
|
|
9474
|
-
</Link>
|
|
9475
|
-
</SidebarMenuButton>
|
|
9476
|
-
</SidebarMenuItem>
|
|
9477
|
-
)
|
|
9298
|
+
return groups
|
|
9478
9299
|
}
|
|
9479
9300
|
|
|
9480
9301
|
export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
9481
9302
|
const session = await getSession()
|
|
9482
9303
|
const settings = await getSetting()
|
|
9483
9304
|
const user = session?.user ?? null
|
|
9305
|
+
const groups = groupNavItems(cmsNavigation)
|
|
9484
9306
|
|
|
9485
9307
|
return (
|
|
9486
9308
|
<Sidebar collapsible="icon" {...props}>
|
|
9487
9309
|
<SidebarHeader className="border-b border-border h-14 items-center flex w-full">
|
|
9488
9310
|
<div className="flex items-center gap-2 w-full relative h-full">
|
|
9489
9311
|
<Link href="/cms" className="flex items-center gap-2 w-full">
|
|
9490
|
-
<Avatar className="size-
|
|
9312
|
+
<Avatar className="size-8">
|
|
9491
9313
|
<AvatarImage src={'/favicon.ico'} />
|
|
9492
|
-
<AvatarFallback className="text-sm font-semibold">
|
|
9314
|
+
<AvatarFallback className="text-sm font-semibold text-foreground">
|
|
9493
9315
|
{settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
|
|
9494
9316
|
</AvatarFallback>
|
|
9495
9317
|
</Avatar>
|
|
9496
|
-
<div className="flex items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
|
|
9497
|
-
<span className="text-sm font-
|
|
9318
|
+
<div className="flex text-foreground items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
|
|
9319
|
+
<span className="text-sm font-medium line-clamp-1">
|
|
9320
|
+
{settings?.siteName ?? cms.name}
|
|
9321
|
+
</span>
|
|
9498
9322
|
</div>
|
|
9499
9323
|
</Link>
|
|
9500
|
-
<SidebarTrigger className="hidden md:flex" />
|
|
9501
9324
|
</div>
|
|
9502
9325
|
</SidebarHeader>
|
|
9503
|
-
<SidebarContent
|
|
9504
|
-
<
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
<
|
|
9513
|
-
|
|
9326
|
+
<SidebarContent>
|
|
9327
|
+
<SidebarGroup className="pb-1">
|
|
9328
|
+
<CmsSearch />
|
|
9329
|
+
</SidebarGroup>
|
|
9330
|
+
|
|
9331
|
+
{groups.map((group) => (
|
|
9332
|
+
<Fragment key={group.label ?? '_ungrouped'}>
|
|
9333
|
+
<SidebarGroup>
|
|
9334
|
+
{group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
|
|
9335
|
+
<SidebarMenu>
|
|
9336
|
+
{group.items.map((item) => (
|
|
9337
|
+
<SidebarMenuItem key={item.href}>
|
|
9338
|
+
<CmsNavLink href={item.href}>
|
|
9339
|
+
{item.icon && (
|
|
9340
|
+
<item.icon className="text-muted-foreground" absoluteStrokeWidth />
|
|
9341
|
+
)}
|
|
9342
|
+
<span>{item.label}</span>
|
|
9343
|
+
</CmsNavLink>
|
|
9344
|
+
</SidebarMenuItem>
|
|
9345
|
+
))}
|
|
9346
|
+
</SidebarMenu>
|
|
9347
|
+
</SidebarGroup>
|
|
9348
|
+
</Fragment>
|
|
9349
|
+
))}
|
|
9350
|
+
|
|
9351
|
+
<SidebarGroup className="mt-auto">
|
|
9352
|
+
<SidebarMenu>
|
|
9353
|
+
<SidebarMenuItem>
|
|
9354
|
+
<CmsNavLink href="/cms/users">
|
|
9355
|
+
<Users />
|
|
9514
9356
|
<span>Users</span>
|
|
9515
|
-
</
|
|
9516
|
-
</
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
<Link href="/cms/settings">
|
|
9521
|
-
<Settings className="size-3.5!" />
|
|
9357
|
+
</CmsNavLink>
|
|
9358
|
+
</SidebarMenuItem>
|
|
9359
|
+
<SidebarMenuItem>
|
|
9360
|
+
<CmsNavLink href="/cms/settings">
|
|
9361
|
+
<Settings />
|
|
9522
9362
|
<span>Settings</span>
|
|
9523
|
-
</
|
|
9524
|
-
</
|
|
9525
|
-
</
|
|
9526
|
-
</
|
|
9363
|
+
</CmsNavLink>
|
|
9364
|
+
</SidebarMenuItem>
|
|
9365
|
+
</SidebarMenu>
|
|
9366
|
+
</SidebarGroup>
|
|
9527
9367
|
</SidebarContent>
|
|
9528
9368
|
<SidebarFooter>
|
|
9529
9369
|
{user && (
|
|
@@ -9535,7 +9375,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
9535
9375
|
</div>
|
|
9536
9376
|
)}
|
|
9537
9377
|
</SidebarFooter>
|
|
9538
|
-
<SidebarRail />
|
|
9539
9378
|
</Sidebar>
|
|
9540
9379
|
)
|
|
9541
9380
|
}
|
|
@@ -9555,7 +9394,7 @@ import {
|
|
|
9555
9394
|
AlertDialogFooter,
|
|
9556
9395
|
AlertDialogHeader,
|
|
9557
9396
|
AlertDialogTitle,
|
|
9558
|
-
AlertDialogTrigger
|
|
9397
|
+
AlertDialogTrigger,
|
|
9559
9398
|
} from '@cms/components/ui/alert-dialog'
|
|
9560
9399
|
import { Button } from '@cms/components/ui/button'
|
|
9561
9400
|
import { Trash2 } from 'lucide-react'
|
|
@@ -9577,7 +9416,7 @@ export function DeleteDialog({
|
|
|
9577
9416
|
isPending = false,
|
|
9578
9417
|
title = 'Are you sure?',
|
|
9579
9418
|
description = 'This action cannot be undone. This will permanently delete this item.',
|
|
9580
|
-
trigger
|
|
9419
|
+
trigger,
|
|
9581
9420
|
}: DeleteDialogProps) {
|
|
9582
9421
|
return (
|
|
9583
9422
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -9626,18 +9465,24 @@ export function DeleteButton({ onClick, label = 'item', count }: DeleteButtonPro
|
|
|
9626
9465
|
function pageHeaderTemplate() {
|
|
9627
9466
|
return `interface PageHeaderProps {
|
|
9628
9467
|
title: string
|
|
9629
|
-
description: string
|
|
9630
9468
|
children?: React.ReactNode
|
|
9469
|
+
search?: React.ReactNode
|
|
9470
|
+
actions?: React.ReactNode
|
|
9471
|
+
back?: React.ReactNode
|
|
9631
9472
|
}
|
|
9632
9473
|
|
|
9633
|
-
export function PageHeader({ title,
|
|
9474
|
+
export function PageHeader({ title, children, search, actions, back }: PageHeaderProps) {
|
|
9634
9475
|
return (
|
|
9635
|
-
<div className="
|
|
9636
|
-
<div className="flex
|
|
9637
|
-
|
|
9638
|
-
<
|
|
9476
|
+
<div className="grid grid-cols-3 items-center justify-between w-full h-14 px-4 border-b border-border">
|
|
9477
|
+
<div className="flex items-center justify-start gap-2 w-full">{back && back}</div>
|
|
9478
|
+
<div className="flex items-center justify-center gap-2">
|
|
9479
|
+
<h2 className="text-sm font-medium tracking-tight">{title}</h2>
|
|
9480
|
+
</div>
|
|
9481
|
+
<div className="flex items-center justify-end gap-2">
|
|
9482
|
+
{children && children}
|
|
9483
|
+
{search && search}
|
|
9484
|
+
{actions && actions}
|
|
9639
9485
|
</div>
|
|
9640
|
-
{children && <div className="flex items-center gap-2">{children}</div>}
|
|
9641
9486
|
</div>
|
|
9642
9487
|
)
|
|
9643
9488
|
}
|
|
@@ -9656,7 +9501,7 @@ const statusStyles: Record<StatusVariant, string> = {
|
|
|
9656
9501
|
success: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
|
|
9657
9502
|
warning: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
|
|
9658
9503
|
error: 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300',
|
|
9659
|
-
info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300'
|
|
9504
|
+
info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
|
|
9660
9505
|
}
|
|
9661
9506
|
|
|
9662
9507
|
interface StatusBadgeProps extends Omit<BadgeProps, 'variant'> {
|
|
@@ -9674,7 +9519,11 @@ export function StatusBadge({ status, className, ...props }: StatusBadgeProps) {
|
|
|
9674
9519
|
}
|
|
9675
9520
|
|
|
9676
9521
|
/** Map boolean values to status badges */
|
|
9677
|
-
export function BooleanBadge({
|
|
9522
|
+
export function BooleanBadge({
|
|
9523
|
+
value,
|
|
9524
|
+
trueLabel = 'Yes',
|
|
9525
|
+
falseLabel = 'No',
|
|
9526
|
+
}: {
|
|
9678
9527
|
value: boolean
|
|
9679
9528
|
trueLabel?: string
|
|
9680
9529
|
falseLabel?: string
|
|
@@ -9698,22 +9547,44 @@ function cmsDataTemplate(projectName) {
|
|
|
9698
9547
|
|
|
9699
9548
|
// src/init/templates/data/navigation.ts
|
|
9700
9549
|
function navigationDataTemplate() {
|
|
9701
|
-
return `import {
|
|
9702
|
-
import
|
|
9550
|
+
return `import type { LucideIcon } from 'lucide-react'
|
|
9551
|
+
import { ChartSpline, FileText, House, ImagePlay, Tag } from 'lucide-react'
|
|
9703
9552
|
|
|
9704
9553
|
export interface CmsNavigationItem {
|
|
9705
9554
|
label: string
|
|
9706
9555
|
href: string
|
|
9707
9556
|
icon?: LucideIcon
|
|
9708
|
-
|
|
9557
|
+
group?: string
|
|
9709
9558
|
}
|
|
9710
9559
|
|
|
9711
9560
|
export const cmsNavigation: CmsNavigationItem[] = [
|
|
9712
9561
|
{
|
|
9713
|
-
label: '
|
|
9562
|
+
label: 'Overview',
|
|
9714
9563
|
href: '/cms',
|
|
9715
|
-
icon: House
|
|
9716
|
-
}
|
|
9564
|
+
icon: House,
|
|
9565
|
+
},
|
|
9566
|
+
{
|
|
9567
|
+
label: 'Analytics',
|
|
9568
|
+
href: '/cms/analytics',
|
|
9569
|
+
icon: ChartSpline,
|
|
9570
|
+
},
|
|
9571
|
+
{
|
|
9572
|
+
label: 'Media',
|
|
9573
|
+
href: '/cms/media',
|
|
9574
|
+
icon: ImagePlay,
|
|
9575
|
+
},
|
|
9576
|
+
{
|
|
9577
|
+
label: 'Categories',
|
|
9578
|
+
href: '/cms/categories',
|
|
9579
|
+
icon: Tag,
|
|
9580
|
+
group: 'Blog',
|
|
9581
|
+
},
|
|
9582
|
+
{
|
|
9583
|
+
label: 'Posts',
|
|
9584
|
+
href: '/cms/posts',
|
|
9585
|
+
icon: FileText,
|
|
9586
|
+
group: 'Blog',
|
|
9587
|
+
},
|
|
9717
9588
|
]
|
|
9718
9589
|
`;
|
|
9719
9590
|
}
|
|
@@ -9787,14 +9658,10 @@ export function CmsThemeProvider({ children }: { children: React.ReactNode }) {
|
|
|
9787
9658
|
|
|
9788
9659
|
const value = React.useMemo(
|
|
9789
9660
|
() => ({ theme, setTheme, resolvedTheme: resolved }),
|
|
9790
|
-
[theme, setTheme, resolved]
|
|
9661
|
+
[theme, setTheme, resolved],
|
|
9791
9662
|
)
|
|
9792
9663
|
|
|
9793
|
-
return
|
|
9794
|
-
<CmsThemeContext.Provider value={value}>
|
|
9795
|
-
{children}
|
|
9796
|
-
</CmsThemeContext.Provider>
|
|
9797
|
-
)
|
|
9664
|
+
return <CmsThemeContext.Provider value={value}>{children}</CmsThemeContext.Provider>
|
|
9798
9665
|
}
|
|
9799
9666
|
|
|
9800
9667
|
export function useTheme(): ThemeContext {
|
|
@@ -9834,11 +9701,11 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9834
9701
|
if (result.success && result.files) {
|
|
9835
9702
|
const images: EditorImageUploadResult[] = result.files.map((f) => ({
|
|
9836
9703
|
url: f.url,
|
|
9837
|
-
filename: f.filename
|
|
9704
|
+
filename: f.filename,
|
|
9838
9705
|
}))
|
|
9839
9706
|
onImagesUploadedRef.current(images)
|
|
9840
9707
|
}
|
|
9841
|
-
}
|
|
9708
|
+
},
|
|
9842
9709
|
})
|
|
9843
9710
|
|
|
9844
9711
|
const isUploading = mutation.isPending
|
|
@@ -9849,21 +9716,19 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9849
9716
|
if (imageFiles.length === 0) return
|
|
9850
9717
|
upload(imageFiles, 'images')
|
|
9851
9718
|
},
|
|
9852
|
-
[upload]
|
|
9719
|
+
[upload],
|
|
9853
9720
|
)
|
|
9854
9721
|
|
|
9855
9722
|
const handleDrop = React.useCallback(
|
|
9856
9723
|
(e: React.DragEvent) => {
|
|
9857
9724
|
e.preventDefault()
|
|
9858
9725
|
e.stopPropagation()
|
|
9859
|
-
const files = Array.from(e.dataTransfer.files).filter((f) =>
|
|
9860
|
-
f.type.startsWith('image/')
|
|
9861
|
-
)
|
|
9726
|
+
const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'))
|
|
9862
9727
|
if (files.length > 0) {
|
|
9863
9728
|
uploadImages(files)
|
|
9864
9729
|
}
|
|
9865
9730
|
},
|
|
9866
|
-
[uploadImages]
|
|
9731
|
+
[uploadImages],
|
|
9867
9732
|
)
|
|
9868
9733
|
|
|
9869
9734
|
const openFilePicker = React.useCallback(() => {
|
|
@@ -9878,7 +9743,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9878
9743
|
}
|
|
9879
9744
|
e.target.value = ''
|
|
9880
9745
|
},
|
|
9881
|
-
[uploadImages]
|
|
9746
|
+
[uploadImages],
|
|
9882
9747
|
)
|
|
9883
9748
|
|
|
9884
9749
|
return {
|
|
@@ -9888,7 +9753,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9888
9753
|
handleDrop,
|
|
9889
9754
|
openFilePicker,
|
|
9890
9755
|
fileInputRef,
|
|
9891
|
-
handleFileInputChange
|
|
9756
|
+
handleFileInputChange,
|
|
9892
9757
|
}
|
|
9893
9758
|
}
|
|
9894
9759
|
`;
|
|
@@ -9912,7 +9777,7 @@ export function useLocalStorage<T>(key: string) {
|
|
|
9912
9777
|
// Silent failure for localStorage access errors
|
|
9913
9778
|
}
|
|
9914
9779
|
},
|
|
9915
|
-
[prefixedKey]
|
|
9780
|
+
[prefixedKey],
|
|
9916
9781
|
)
|
|
9917
9782
|
|
|
9918
9783
|
const getItem = React.useCallback((): T | null => {
|
|
@@ -9945,18 +9810,36 @@ export function useLocalStorage<T>(key: string) {
|
|
|
9945
9810
|
`;
|
|
9946
9811
|
}
|
|
9947
9812
|
|
|
9813
|
+
// src/init/templates/hooks/use-mobile.ts
|
|
9814
|
+
function useMobileHookTemplate() {
|
|
9815
|
+
return `import * as React from 'react'
|
|
9816
|
+
|
|
9817
|
+
const MOBILE_BREAKPOINT = 768
|
|
9818
|
+
|
|
9819
|
+
export function useIsMobile() {
|
|
9820
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
9821
|
+
|
|
9822
|
+
React.useEffect(() => {
|
|
9823
|
+
const mql = window.matchMedia(\`(max-width: \${MOBILE_BREAKPOINT - 1}px)\`)
|
|
9824
|
+
const onChange = () => {
|
|
9825
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
9826
|
+
}
|
|
9827
|
+
mql.addEventListener('change', onChange)
|
|
9828
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
9829
|
+
return () => mql.removeEventListener('change', onChange)
|
|
9830
|
+
}, [])
|
|
9831
|
+
|
|
9832
|
+
return !!isMobile
|
|
9833
|
+
}
|
|
9834
|
+
`;
|
|
9835
|
+
}
|
|
9836
|
+
|
|
9948
9837
|
// src/init/templates/hooks/use-upload.ts
|
|
9949
9838
|
function useUploadHookTemplate() {
|
|
9950
9839
|
return `'use client'
|
|
9951
9840
|
|
|
9952
|
-
import type {
|
|
9953
|
-
|
|
9954
|
-
UploadProgress
|
|
9955
|
-
} from '@cms/types'
|
|
9956
|
-
import {
|
|
9957
|
-
type FileValidationConfig,
|
|
9958
|
-
validateFiles
|
|
9959
|
-
} from '@cms/utils/validation'
|
|
9841
|
+
import type { UploadFileResult, UploadProgress } from '@cms/types'
|
|
9842
|
+
import { type FileValidationConfig, validateFiles } from '@cms/utils/validation'
|
|
9960
9843
|
import { type UseMutationResult, useMutation } from '@tanstack/react-query'
|
|
9961
9844
|
import * as React from 'react'
|
|
9962
9845
|
|
|
@@ -10004,7 +9887,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10004
9887
|
onProgress,
|
|
10005
9888
|
onSuccess,
|
|
10006
9889
|
onError,
|
|
10007
|
-
prefix: defaultPrefix
|
|
9890
|
+
prefix: defaultPrefix,
|
|
10008
9891
|
} = options
|
|
10009
9892
|
|
|
10010
9893
|
const validationConfig = React.useMemo<FileValidationConfig>(() => {
|
|
@@ -10014,7 +9897,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10014
9897
|
const parsedTypes = parseAcceptTypes(accept)
|
|
10015
9898
|
if (config.allowedTypes && config.allowedTypes.length > 0) {
|
|
10016
9899
|
config.allowedTypes = [...config.allowedTypes, ...parsedTypes].filter(
|
|
10017
|
-
(v, i, a) => a.indexOf(v) === i
|
|
9900
|
+
(v, i, a) => a.indexOf(v) === i,
|
|
10018
9901
|
)
|
|
10019
9902
|
} else {
|
|
10020
9903
|
config.allowedTypes = parsedTypes
|
|
@@ -10037,7 +9920,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10037
9920
|
filename: file.name,
|
|
10038
9921
|
progress: 0,
|
|
10039
9922
|
loaded: 0,
|
|
10040
|
-
total: file.size
|
|
9923
|
+
total: file.size,
|
|
10041
9924
|
}))
|
|
10042
9925
|
setProgress(initialProgress)
|
|
10043
9926
|
onProgress?.(initialProgress)
|
|
@@ -10059,9 +9942,9 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10059
9942
|
return {
|
|
10060
9943
|
...p,
|
|
10061
9944
|
progress: newProgress,
|
|
10062
|
-
loaded: Math.floor((p.total * newProgress) / 100)
|
|
9945
|
+
loaded: Math.floor((p.total * newProgress) / 100),
|
|
10063
9946
|
}
|
|
10064
|
-
})
|
|
9947
|
+
}),
|
|
10065
9948
|
)
|
|
10066
9949
|
}, 200)
|
|
10067
9950
|
|
|
@@ -10075,7 +9958,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10075
9958
|
|
|
10076
9959
|
const response = await fetch('/api/cms/upload', {
|
|
10077
9960
|
method: 'POST',
|
|
10078
|
-
body: formData
|
|
9961
|
+
body: formData,
|
|
10079
9962
|
})
|
|
10080
9963
|
|
|
10081
9964
|
const result = (await response.json()) as UploadFileResult
|
|
@@ -10085,7 +9968,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10085
9968
|
filename: file.name,
|
|
10086
9969
|
progress: 100,
|
|
10087
9970
|
loaded: file.size,
|
|
10088
|
-
total: file.size
|
|
9971
|
+
total: file.size,
|
|
10089
9972
|
}))
|
|
10090
9973
|
setProgress(completeProgress)
|
|
10091
9974
|
onProgress?.(completeProgress)
|
|
@@ -10107,14 +9990,14 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10107
9990
|
onError: (error) => {
|
|
10108
9991
|
onError?.(error)
|
|
10109
9992
|
setProgress([])
|
|
10110
|
-
}
|
|
9993
|
+
},
|
|
10111
9994
|
})
|
|
10112
9995
|
|
|
10113
9996
|
const upload = React.useCallback(
|
|
10114
9997
|
(files: File[], prefix?: string) => {
|
|
10115
9998
|
mutation.mutate({ files, prefix })
|
|
10116
9999
|
},
|
|
10117
|
-
[mutation]
|
|
10000
|
+
[mutation],
|
|
10118
10001
|
)
|
|
10119
10002
|
|
|
10120
10003
|
const validate = React.useCallback(
|
|
@@ -10122,10 +10005,10 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10122
10005
|
const result = validateFiles(files, validationConfig)
|
|
10123
10006
|
return {
|
|
10124
10007
|
valid: result.valid,
|
|
10125
|
-
errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`)
|
|
10008
|
+
errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`),
|
|
10126
10009
|
}
|
|
10127
10010
|
},
|
|
10128
|
-
[validationConfig]
|
|
10011
|
+
[validationConfig],
|
|
10129
10012
|
)
|
|
10130
10013
|
|
|
10131
10014
|
return { mutation, progress, upload, validate }
|
|
@@ -10145,7 +10028,7 @@ export function useUsers() {
|
|
|
10145
10028
|
return useQuery<UsersResponse>({
|
|
10146
10029
|
queryKey: ['users'],
|
|
10147
10030
|
queryFn: () => getUsers(),
|
|
10148
|
-
staleTime: 0
|
|
10031
|
+
staleTime: 0,
|
|
10149
10032
|
})
|
|
10150
10033
|
}
|
|
10151
10034
|
`;
|
|
@@ -10155,9 +10038,9 @@ export function useUsers() {
|
|
|
10155
10038
|
function formSettingsActionTemplate() {
|
|
10156
10039
|
return `'use server'
|
|
10157
10040
|
|
|
10158
|
-
import { eq } from 'drizzle-orm'
|
|
10159
10041
|
import db from '@cms/db'
|
|
10160
10042
|
import { formSettings } from '@cms/db/schema'
|
|
10043
|
+
import { eq } from 'drizzle-orm'
|
|
10161
10044
|
|
|
10162
10045
|
export interface FormSettingsData {
|
|
10163
10046
|
id: number
|
|
@@ -10181,9 +10064,7 @@ export interface FormSettingsResult {
|
|
|
10181
10064
|
settings?: FormSettingsData
|
|
10182
10065
|
}
|
|
10183
10066
|
|
|
10184
|
-
export async function getFormSettings(
|
|
10185
|
-
formName: string
|
|
10186
|
-
): Promise<FormSettingsData | null> {
|
|
10067
|
+
export async function getFormSettings(formName: string): Promise<FormSettingsData | null> {
|
|
10187
10068
|
try {
|
|
10188
10069
|
const [settings] = await db
|
|
10189
10070
|
.select()
|
|
@@ -10199,7 +10080,7 @@ export async function getFormSettings(
|
|
|
10199
10080
|
|
|
10200
10081
|
export async function upsertFormSettings(
|
|
10201
10082
|
formName: string,
|
|
10202
|
-
data: UpsertFormSettingsInput
|
|
10083
|
+
data: UpsertFormSettingsInput,
|
|
10203
10084
|
): Promise<FormSettingsResult> {
|
|
10204
10085
|
try {
|
|
10205
10086
|
const existing = await getFormSettings(formName)
|
|
@@ -10230,8 +10111,7 @@ export async function upsertFormSettings(
|
|
|
10230
10111
|
console.error(\`Error upserting form settings for \${formName}:\`, error)
|
|
10231
10112
|
return {
|
|
10232
10113
|
success: false,
|
|
10233
|
-
error:
|
|
10234
|
-
error instanceof Error ? error.message : 'Failed to save form settings',
|
|
10114
|
+
error: error instanceof Error ? error.message : 'Failed to save form settings',
|
|
10235
10115
|
}
|
|
10236
10116
|
}
|
|
10237
10117
|
}
|
|
@@ -10247,7 +10127,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
|
|
|
10247
10127
|
}
|
|
10248
10128
|
|
|
10249
10129
|
export async function testFormWebhook(
|
|
10250
|
-
formName: string
|
|
10130
|
+
formName: string,
|
|
10251
10131
|
): Promise<{ success: boolean; error?: string }> {
|
|
10252
10132
|
try {
|
|
10253
10133
|
const settings = await getFormSettings(formName)
|
|
@@ -10278,8 +10158,7 @@ export async function testFormWebhook(
|
|
|
10278
10158
|
} catch (error) {
|
|
10279
10159
|
return {
|
|
10280
10160
|
success: false,
|
|
10281
|
-
error:
|
|
10282
|
-
error instanceof Error ? error.message : 'Failed to send test webhook',
|
|
10161
|
+
error: error instanceof Error ? error.message : 'Failed to send test webhook',
|
|
10283
10162
|
}
|
|
10284
10163
|
}
|
|
10285
10164
|
}
|
|
@@ -10448,10 +10327,10 @@ export async function uploadImageFromUrl(
|
|
|
10448
10327
|
function usersActionTemplate() {
|
|
10449
10328
|
return `'use server'
|
|
10450
10329
|
|
|
10330
|
+
import { auth } from '@cms/auth'
|
|
10451
10331
|
import db from '@cms/db'
|
|
10452
|
-
import { user
|
|
10332
|
+
import { user } from '@cms/db/schema'
|
|
10453
10333
|
import { eq } from 'drizzle-orm'
|
|
10454
|
-
import { auth } from '@cms/auth'
|
|
10455
10334
|
|
|
10456
10335
|
export interface UserData {
|
|
10457
10336
|
id: string
|
|
@@ -10564,15 +10443,9 @@ export async function getUsers(): Promise<UsersResponse> {
|
|
|
10564
10443
|
/**
|
|
10565
10444
|
* Update a user's role
|
|
10566
10445
|
*/
|
|
10567
|
-
export async function updateUserRole(
|
|
10568
|
-
userId: string,
|
|
10569
|
-
role: string,
|
|
10570
|
-
): Promise<UpdateUserRoleResult> {
|
|
10446
|
+
export async function updateUserRole(userId: string, role: string): Promise<UpdateUserRoleResult> {
|
|
10571
10447
|
try {
|
|
10572
|
-
await db
|
|
10573
|
-
.update(user)
|
|
10574
|
-
.set({ role, updatedAt: new Date() })
|
|
10575
|
-
.where(eq(user.id, userId))
|
|
10448
|
+
await db.update(user).set({ role, updatedAt: new Date() }).where(eq(user.id, userId))
|
|
10576
10449
|
|
|
10577
10450
|
return { success: true }
|
|
10578
10451
|
} catch (error) {
|
|
@@ -10722,10 +10595,21 @@ function trimMathBlock(content: string): string {
|
|
|
10722
10595
|
const shiki = createHighlighterCoreSync({
|
|
10723
10596
|
themes: [githubDark, githubLight],
|
|
10724
10597
|
langs: [
|
|
10725
|
-
javascript,
|
|
10726
|
-
|
|
10598
|
+
javascript,
|
|
10599
|
+
typescript,
|
|
10600
|
+
jsx,
|
|
10601
|
+
tsx,
|
|
10602
|
+
python,
|
|
10603
|
+
rust,
|
|
10604
|
+
go,
|
|
10605
|
+
json,
|
|
10606
|
+
yaml,
|
|
10607
|
+
css,
|
|
10608
|
+
sql,
|
|
10609
|
+
shellscript,
|
|
10610
|
+
markdown,
|
|
10727
10611
|
],
|
|
10728
|
-
engine: createJavaScriptRegexEngine()
|
|
10612
|
+
engine: createJavaScriptRegexEngine(),
|
|
10729
10613
|
})
|
|
10730
10614
|
|
|
10731
10615
|
const loadedLangs = shiki.getLoadedLanguages()
|
|
@@ -10767,11 +10651,11 @@ const md = MarkdownIt({
|
|
|
10767
10651
|
lang: language,
|
|
10768
10652
|
themes: { light: 'github-light', dark: 'github-dark' },
|
|
10769
10653
|
defaultColor: false,
|
|
10770
|
-
transformers: [transformerNotationHighlight(), transformerNotationDiff()]
|
|
10654
|
+
transformers: [transformerNotationHighlight(), transformerNotationDiff()],
|
|
10771
10655
|
})
|
|
10772
10656
|
const escapedCode = code.replace(/</g, '<').replace(/>/g, '>')
|
|
10773
10657
|
return \`<div class="code-block-wrapper not-prose" data-lang="\${language}"><button class="copy-button" data-code="\${escapedCode.replace(/"/g, '"')}" aria-label="Copy code">Copy</button>\${highlighted}</div>\`
|
|
10774
|
-
}
|
|
10658
|
+
},
|
|
10775
10659
|
})
|
|
10776
10660
|
.use(headingAnchorPlugin)
|
|
10777
10661
|
.use(dollarmath, {
|
|
@@ -10784,7 +10668,7 @@ const md = MarkdownIt({
|
|
|
10784
10668
|
return renderToString(content, {
|
|
10785
10669
|
displayMode,
|
|
10786
10670
|
throwOnError: false,
|
|
10787
|
-
strict: 'ignore'
|
|
10671
|
+
strict: 'ignore',
|
|
10788
10672
|
})
|
|
10789
10673
|
},
|
|
10790
10674
|
labelNormalizer(label: string) {
|
|
@@ -10792,7 +10676,7 @@ const md = MarkdownIt({
|
|
|
10792
10676
|
},
|
|
10793
10677
|
labelRenderer(label: string) {
|
|
10794
10678
|
return \`<a href="#\${label}" class="mathlabel" title="Permalink to this equation">\xB6</a>\`
|
|
10795
|
-
}
|
|
10679
|
+
},
|
|
10796
10680
|
})
|
|
10797
10681
|
|
|
10798
10682
|
export function renderMarkdownSync(src: string): string {
|
|
@@ -10887,7 +10771,7 @@ export interface AuthSession {
|
|
|
10887
10771
|
export enum UserRole {
|
|
10888
10772
|
ADMIN = 'admin',
|
|
10889
10773
|
EDITOR = 'editor',
|
|
10890
|
-
MEMBER = 'member'
|
|
10774
|
+
MEMBER = 'member',
|
|
10891
10775
|
}
|
|
10892
10776
|
|
|
10893
10777
|
export interface UserWithRole extends AuthUser {
|
|
@@ -10896,17 +10780,11 @@ export interface UserWithRole extends AuthUser {
|
|
|
10896
10780
|
|
|
10897
10781
|
/** Type guard to check if a value is a valid UserRole */
|
|
10898
10782
|
export function isUserRole(value: unknown): value is UserRole {
|
|
10899
|
-
return (
|
|
10900
|
-
typeof value === 'string' &&
|
|
10901
|
-
Object.values(UserRole).includes(value as UserRole)
|
|
10902
|
-
)
|
|
10783
|
+
return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
|
|
10903
10784
|
}
|
|
10904
10785
|
|
|
10905
10786
|
/** Check if user has one of the allowed roles */
|
|
10906
|
-
export function hasRequiredRole(
|
|
10907
|
-
userRole: UserRole,
|
|
10908
|
-
allowedRoles: UserRole[]
|
|
10909
|
-
): boolean {
|
|
10787
|
+
export function hasRequiredRole(userRole: UserRole, allowedRoles: UserRole[]): boolean {
|
|
10910
10788
|
return allowedRoles.includes(userRole)
|
|
10911
10789
|
}
|
|
10912
10790
|
|
|
@@ -11092,7 +10970,7 @@ export function createMetadata({
|
|
|
11092
10970
|
description,
|
|
11093
10971
|
path,
|
|
11094
10972
|
ogImage,
|
|
11095
|
-
noIndex = false
|
|
10973
|
+
noIndex = false,
|
|
11096
10974
|
}: CreateMetadataOptions): Metadata {
|
|
11097
10975
|
const metadata: Metadata = {
|
|
11098
10976
|
title,
|
|
@@ -11101,14 +10979,14 @@ export function createMetadata({
|
|
|
11101
10979
|
title,
|
|
11102
10980
|
description,
|
|
11103
10981
|
type: 'website',
|
|
11104
|
-
...(ogImage && { images: [{ url: ogImage }] })
|
|
10982
|
+
...(ogImage && { images: [{ url: ogImage }] }),
|
|
11105
10983
|
},
|
|
11106
10984
|
twitter: {
|
|
11107
10985
|
card: ogImage ? 'summary_large_image' : 'summary',
|
|
11108
10986
|
title,
|
|
11109
10987
|
description,
|
|
11110
|
-
...(ogImage && { images: [ogImage] })
|
|
11111
|
-
}
|
|
10988
|
+
...(ogImage && { images: [ogImage] }),
|
|
10989
|
+
},
|
|
11112
10990
|
}
|
|
11113
10991
|
|
|
11114
10992
|
if (path) {
|
|
@@ -11132,7 +11010,7 @@ export function generateArticleSchema({
|
|
|
11132
11010
|
imageUrl,
|
|
11133
11011
|
datePublished,
|
|
11134
11012
|
dateModified,
|
|
11135
|
-
authorName
|
|
11013
|
+
authorName,
|
|
11136
11014
|
}: {
|
|
11137
11015
|
title: string
|
|
11138
11016
|
description: string
|
|
@@ -11152,8 +11030,8 @@ export function generateArticleSchema({
|
|
|
11152
11030
|
datePublished,
|
|
11153
11031
|
...(dateModified && { dateModified }),
|
|
11154
11032
|
...(authorName && {
|
|
11155
|
-
author: { '@type': 'Person', name: authorName }
|
|
11156
|
-
})
|
|
11033
|
+
author: { '@type': 'Person', name: authorName },
|
|
11034
|
+
}),
|
|
11157
11035
|
}
|
|
11158
11036
|
}
|
|
11159
11037
|
|
|
@@ -11206,20 +11084,16 @@ function isFileTypeAllowed(file: File, allowedTypes: string[]): boolean {
|
|
|
11206
11084
|
*/
|
|
11207
11085
|
export function validateFiles(
|
|
11208
11086
|
files: File[],
|
|
11209
|
-
config: FileValidationConfig = {}
|
|
11087
|
+
config: FileValidationConfig = {},
|
|
11210
11088
|
): FileValidationResult {
|
|
11211
|
-
const {
|
|
11212
|
-
maxSizeInBytes = DEFAULT_MAX_SIZE,
|
|
11213
|
-
allowedTypes,
|
|
11214
|
-
maxFiles = DEFAULT_MAX_FILES
|
|
11215
|
-
} = config
|
|
11089
|
+
const { maxSizeInBytes = DEFAULT_MAX_SIZE, allowedTypes, maxFiles = DEFAULT_MAX_FILES } = config
|
|
11216
11090
|
|
|
11217
11091
|
const errors: FileValidationError[] = []
|
|
11218
11092
|
|
|
11219
11093
|
if (files.length > maxFiles) {
|
|
11220
11094
|
errors.push({
|
|
11221
11095
|
filename: '',
|
|
11222
|
-
error: \`Too many files. Maximum is \${maxFiles}
|
|
11096
|
+
error: \`Too many files. Maximum is \${maxFiles}.\`,
|
|
11223
11097
|
})
|
|
11224
11098
|
}
|
|
11225
11099
|
|
|
@@ -11228,14 +11102,14 @@ export function validateFiles(
|
|
|
11228
11102
|
const maxMB = Math.round(maxSizeInBytes / (1024 * 1024))
|
|
11229
11103
|
errors.push({
|
|
11230
11104
|
filename: file.name,
|
|
11231
|
-
error: \`File exceeds maximum size of \${maxMB}MB
|
|
11105
|
+
error: \`File exceeds maximum size of \${maxMB}MB.\`,
|
|
11232
11106
|
})
|
|
11233
11107
|
}
|
|
11234
11108
|
|
|
11235
11109
|
if (allowedTypes && allowedTypes.length > 0 && !isFileTypeAllowed(file, allowedTypes)) {
|
|
11236
11110
|
errors.push({
|
|
11237
11111
|
filename: file.name,
|
|
11238
|
-
error: \`File type "\${file.type || 'unknown'}" is not allowed
|
|
11112
|
+
error: \`File type "\${file.type || 'unknown'}" is not allowed.\`,
|
|
11239
11113
|
})
|
|
11240
11114
|
}
|
|
11241
11115
|
}
|
|
@@ -11289,17 +11163,15 @@ function webhookUtilTemplate() {
|
|
|
11289
11163
|
*/
|
|
11290
11164
|
export function sendWebhook(
|
|
11291
11165
|
webhookUrl: string | null | undefined,
|
|
11292
|
-
payload: Record<string, unknown
|
|
11166
|
+
payload: Record<string, unknown>,
|
|
11293
11167
|
): void {
|
|
11294
|
-
if (!webhookUrl) return
|
|
11295
|
-
// Fire-and-forget: runs in background, doesn't block
|
|
11168
|
+
if (!webhookUrl) return // Fire-and-forget: runs in background, doesn't block
|
|
11296
11169
|
;(async () => {
|
|
11297
11170
|
try {
|
|
11298
11171
|
const formData = new URLSearchParams()
|
|
11299
11172
|
for (const [key, value] of Object.entries(payload)) {
|
|
11300
11173
|
if (value === null || value === undefined) continue
|
|
11301
|
-
const stringValue =
|
|
11302
|
-
typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
11174
|
+
const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
11303
11175
|
formData.append(key, stringValue)
|
|
11304
11176
|
}
|
|
11305
11177
|
await fetch(webhookUrl, {
|
|
@@ -11327,6 +11199,7 @@ function scaffoldComponents({ cwd, config }) {
|
|
|
11327
11199
|
}
|
|
11328
11200
|
write("cms-globals.css", cmsGlobalsCssTemplate());
|
|
11329
11201
|
write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
|
|
11202
|
+
write("components/layout/cms-nav-link.tsx", cmsNavLinkTemplate());
|
|
11330
11203
|
write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
|
|
11331
11204
|
write("components/layout/cms-header.tsx", cmsHeaderTemplate());
|
|
11332
11205
|
write("components/layout/cms-search.tsx", cmsSearchTemplate());
|
|
@@ -11352,6 +11225,7 @@ function scaffoldComponents({ cwd, config }) {
|
|
|
11352
11225
|
write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
|
|
11353
11226
|
write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
|
|
11354
11227
|
write("hooks/use-users.ts", useUsersHookTemplate());
|
|
11228
|
+
write("hooks/use-mobile.ts", useMobileHookTemplate());
|
|
11355
11229
|
const projectName = detectProjectName(cwd);
|
|
11356
11230
|
write("data/cms.ts", cmsDataTemplate(projectName));
|
|
11357
11231
|
write("data/navigation.ts", navigationDataTemplate());
|
|
@@ -11588,6 +11462,7 @@ var CORE_DEPS = [
|
|
|
11588
11462
|
"nuqs",
|
|
11589
11463
|
"sonner",
|
|
11590
11464
|
// Styling utilities
|
|
11465
|
+
"geist",
|
|
11591
11466
|
"class-variance-authority",
|
|
11592
11467
|
"clsx",
|
|
11593
11468
|
"tailwind-merge",
|
|
@@ -11833,24 +11708,18 @@ import path32 from "path";
|
|
|
11833
11708
|
|
|
11834
11709
|
// src/init/templates/pages/authenticated-layout.ts
|
|
11835
11710
|
function authenticatedLayoutTemplate() {
|
|
11836
|
-
return `import {
|
|
11711
|
+
return `import { requireRole } from '@cms/auth/middleware'
|
|
11837
11712
|
import { CmsSidebar } from '@cms/components/layout/cms-sidebar'
|
|
11838
|
-
import { requireRole } from '@cms/auth/middleware'
|
|
11839
|
-
import { UserRole } from '@cms/types/auth'
|
|
11840
11713
|
import { SidebarInset, SidebarProvider } from '@cms/components/ui/sidebar'
|
|
11714
|
+
import { UserRole } from '@cms/types/auth'
|
|
11841
11715
|
|
|
11842
|
-
export default async function CmsAuthLayout({
|
|
11843
|
-
children
|
|
11844
|
-
}: {
|
|
11845
|
-
children: React.ReactNode
|
|
11846
|
-
}) {
|
|
11716
|
+
export default async function CmsAuthLayout({ children }: { children: React.ReactNode }) {
|
|
11847
11717
|
await requireRole([UserRole.ADMIN, UserRole.EDITOR])
|
|
11848
11718
|
|
|
11849
11719
|
return (
|
|
11850
11720
|
<SidebarProvider>
|
|
11851
11721
|
<CmsSidebar />
|
|
11852
11722
|
<SidebarInset>
|
|
11853
|
-
<CmsHeader />
|
|
11854
11723
|
<main>{children}</main>
|
|
11855
11724
|
</SidebarInset>
|
|
11856
11725
|
</SidebarProvider>
|
|
@@ -11863,11 +11732,17 @@ export default async function CmsAuthLayout({
|
|
|
11863
11732
|
function cmsLayoutTemplate() {
|
|
11864
11733
|
return `import '@cms/cms-globals.css'
|
|
11865
11734
|
import { CmsProviders } from '@cms/components/layout/cms-providers'
|
|
11735
|
+
import { GeistMono } from 'geist/font/mono'
|
|
11736
|
+
import { GeistSans } from 'geist/font/sans'
|
|
11866
11737
|
|
|
11867
11738
|
export default function CmsLayout({ children }: { children: React.ReactNode }) {
|
|
11868
11739
|
return (
|
|
11869
11740
|
<CmsProviders>
|
|
11870
|
-
<div
|
|
11741
|
+
<div
|
|
11742
|
+
className={\`cms-root min-h-screen antialiased \${GeistSans.variable} \${GeistMono.variable}\`}
|
|
11743
|
+
>
|
|
11744
|
+
{children}
|
|
11745
|
+
</div>
|
|
11871
11746
|
</CmsProviders>
|
|
11872
11747
|
)
|
|
11873
11748
|
}
|
|
@@ -11877,8 +11752,8 @@ export default function CmsLayout({ children }: { children: React.ReactNode }) {
|
|
|
11877
11752
|
// src/init/templates/pages/dashboard-page.ts
|
|
11878
11753
|
function dashboardPageTemplate() {
|
|
11879
11754
|
return `import { PageHeader } from '@cms/components/shared/page-header'
|
|
11880
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
|
|
11881
11755
|
import { Badge } from '@cms/components/ui/badge'
|
|
11756
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
|
|
11882
11757
|
import { FileText, Settings, Users } from 'lucide-react'
|
|
11883
11758
|
import Link from 'next/link'
|
|
11884
11759
|
|
|
@@ -11887,32 +11762,27 @@ const quickLinks = [
|
|
|
11887
11762
|
title: 'Users',
|
|
11888
11763
|
description: 'Manage admin users and roles',
|
|
11889
11764
|
href: '/cms/users',
|
|
11890
|
-
icon: Users
|
|
11765
|
+
icon: Users,
|
|
11891
11766
|
},
|
|
11892
11767
|
{
|
|
11893
11768
|
title: 'Settings',
|
|
11894
11769
|
description: 'Configure CMS settings',
|
|
11895
11770
|
href: '/cms/settings',
|
|
11896
|
-
icon: Settings
|
|
11771
|
+
icon: Settings,
|
|
11897
11772
|
},
|
|
11898
11773
|
{
|
|
11899
11774
|
title: 'Generate',
|
|
11900
11775
|
description: 'Add a new resource from a schema',
|
|
11901
11776
|
href: '#',
|
|
11902
11777
|
icon: FileText,
|
|
11903
|
-
hint: 'npx betterstart generate <schema>'
|
|
11904
|
-
}
|
|
11778
|
+
hint: 'npx betterstart generate <schema>',
|
|
11779
|
+
},
|
|
11905
11780
|
]
|
|
11906
11781
|
|
|
11907
11782
|
export default function DashboardPage() {
|
|
11908
11783
|
return (
|
|
11909
11784
|
<div className="flex flex-col">
|
|
11910
|
-
<
|
|
11911
|
-
<PageHeader
|
|
11912
|
-
title="Dashboard"
|
|
11913
|
-
description="Welcome to your CMS admin panel"
|
|
11914
|
-
/>
|
|
11915
|
-
</div>
|
|
11785
|
+
<PageHeader title="Dashboard" />
|
|
11916
11786
|
<div className="p-6 space-y-6">
|
|
11917
11787
|
<div className="grid gap-4 md:grid-cols-3">
|
|
11918
11788
|
{quickLinks.map((link) => (
|
|
@@ -11982,7 +11852,7 @@ import { LoginForm } from './login-form'
|
|
|
11982
11852
|
|
|
11983
11853
|
export const metadata: Metadata = {
|
|
11984
11854
|
title: 'CMS Login',
|
|
11985
|
-
robots: { index: false, follow: false }
|
|
11855
|
+
robots: { index: false, follow: false },
|
|
11986
11856
|
}
|
|
11987
11857
|
|
|
11988
11858
|
export default function LoginPage() {
|
|
@@ -11991,9 +11861,7 @@ export default function LoginPage() {
|
|
|
11991
11861
|
<div className="w-full max-w-sm">
|
|
11992
11862
|
<div className="mb-8 text-center">
|
|
11993
11863
|
<h1 className="text-2xl font-semibold tracking-tight">CMS</h1>
|
|
11994
|
-
<p className="text-muted-foreground text-sm mt-1">
|
|
11995
|
-
Sign in to access the admin panel
|
|
11996
|
-
</p>
|
|
11864
|
+
<p className="text-muted-foreground text-sm mt-1">Sign in to access the admin panel</p>
|
|
11997
11865
|
</div>
|
|
11998
11866
|
<LoginForm />
|
|
11999
11867
|
</div>
|
|
@@ -12028,7 +11896,7 @@ export function LoginForm() {
|
|
|
12028
11896
|
try {
|
|
12029
11897
|
const result = await authClient.signIn.email({
|
|
12030
11898
|
email,
|
|
12031
|
-
password
|
|
11899
|
+
password,
|
|
12032
11900
|
})
|
|
12033
11901
|
|
|
12034
11902
|
if (result.error) {
|
|
@@ -12048,9 +11916,7 @@ export function LoginForm() {
|
|
|
12048
11916
|
return (
|
|
12049
11917
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
12050
11918
|
{error && (
|
|
12051
|
-
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
|
|
12052
|
-
{error}
|
|
12053
|
-
</div>
|
|
11919
|
+
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">{error}</div>
|
|
12054
11920
|
)}
|
|
12055
11921
|
|
|
12056
11922
|
<div className="space-y-2">
|
|
@@ -12095,6 +11961,7 @@ export function LoginForm() {
|
|
|
12095
11961
|
function createUserDialogTemplate() {
|
|
12096
11962
|
return `'use client'
|
|
12097
11963
|
|
|
11964
|
+
import { createUser } from '@cms/actions/users'
|
|
12098
11965
|
import { Button } from '@cms/components/ui/button'
|
|
12099
11966
|
import {
|
|
12100
11967
|
Dialog,
|
|
@@ -12102,11 +11969,10 @@ import {
|
|
|
12102
11969
|
DialogDescription,
|
|
12103
11970
|
DialogHeader,
|
|
12104
11971
|
DialogTitle,
|
|
12105
|
-
DialogTrigger
|
|
11972
|
+
DialogTrigger,
|
|
12106
11973
|
} from '@cms/components/ui/dialog'
|
|
12107
11974
|
import { Input } from '@cms/components/ui/input'
|
|
12108
11975
|
import { Label } from '@cms/components/ui/label'
|
|
12109
|
-
import { createUser } from '@cms/actions/users'
|
|
12110
11976
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12111
11977
|
import { Loader2, UserPlus } from 'lucide-react'
|
|
12112
11978
|
import * as React from 'react'
|
|
@@ -12216,6 +12082,7 @@ export function CreateUserDialog() {
|
|
|
12216
12082
|
function editRoleDialogTemplate() {
|
|
12217
12083
|
return `'use client'
|
|
12218
12084
|
|
|
12085
|
+
import { updateUserRole } from '@cms/actions/users'
|
|
12219
12086
|
import { Button } from '@cms/components/ui/button'
|
|
12220
12087
|
import {
|
|
12221
12088
|
Dialog,
|
|
@@ -12223,7 +12090,7 @@ import {
|
|
|
12223
12090
|
DialogDescription,
|
|
12224
12091
|
DialogHeader,
|
|
12225
12092
|
DialogTitle,
|
|
12226
|
-
DialogTrigger
|
|
12093
|
+
DialogTrigger,
|
|
12227
12094
|
} from '@cms/components/ui/dialog'
|
|
12228
12095
|
import { Label } from '@cms/components/ui/label'
|
|
12229
12096
|
import {
|
|
@@ -12231,9 +12098,8 @@ import {
|
|
|
12231
12098
|
SelectContent,
|
|
12232
12099
|
SelectItem,
|
|
12233
12100
|
SelectTrigger,
|
|
12234
|
-
SelectValue
|
|
12101
|
+
SelectValue,
|
|
12235
12102
|
} from '@cms/components/ui/select'
|
|
12236
|
-
import { updateUserRole } from '@cms/actions/users'
|
|
12237
12103
|
import { UserRole } from '@cms/types/auth'
|
|
12238
12104
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12239
12105
|
import { Loader2 } from 'lucide-react'
|
|
@@ -12247,12 +12113,7 @@ interface EditRoleDialogProps {
|
|
|
12247
12113
|
children: React.ReactNode
|
|
12248
12114
|
}
|
|
12249
12115
|
|
|
12250
|
-
export function EditRoleDialog({
|
|
12251
|
-
userId,
|
|
12252
|
-
currentRole,
|
|
12253
|
-
userName,
|
|
12254
|
-
children
|
|
12255
|
-
}: EditRoleDialogProps) {
|
|
12116
|
+
export function EditRoleDialog({ userId, currentRole, userName, children }: EditRoleDialogProps) {
|
|
12256
12117
|
const [open, setOpen] = React.useState(false)
|
|
12257
12118
|
const [role, setRole] = React.useState(currentRole)
|
|
12258
12119
|
const [isPending, startTransition] = React.useTransition()
|
|
@@ -12281,9 +12142,7 @@ export function EditRoleDialog({
|
|
|
12281
12142
|
<DialogContent className="sm:max-w-[350px]">
|
|
12282
12143
|
<DialogHeader>
|
|
12283
12144
|
<DialogTitle>Edit Role</DialogTitle>
|
|
12284
|
-
<DialogDescription>
|
|
12285
|
-
Change the role for {userName}
|
|
12286
|
-
</DialogDescription>
|
|
12145
|
+
<DialogDescription>Change the role for {userName}</DialogDescription>
|
|
12287
12146
|
</DialogHeader>
|
|
12288
12147
|
<div className="space-y-4">
|
|
12289
12148
|
<div className="space-y-2">
|
|
@@ -12300,17 +12159,10 @@ export function EditRoleDialog({
|
|
|
12300
12159
|
</Select>
|
|
12301
12160
|
</div>
|
|
12302
12161
|
<div className="flex justify-end gap-2">
|
|
12303
|
-
<Button
|
|
12304
|
-
variant="outline"
|
|
12305
|
-
onClick={() => setOpen(false)}
|
|
12306
|
-
disabled={isPending}
|
|
12307
|
-
>
|
|
12162
|
+
<Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
|
12308
12163
|
Cancel
|
|
12309
12164
|
</Button>
|
|
12310
|
-
<Button
|
|
12311
|
-
onClick={handleSave}
|
|
12312
|
-
disabled={isPending || role === currentRole}
|
|
12313
|
-
>
|
|
12165
|
+
<Button onClick={handleSave} disabled={isPending || role === currentRole}>
|
|
12314
12166
|
{isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
|
|
12315
12167
|
Save
|
|
12316
12168
|
</Button>
|
|
@@ -12327,11 +12179,7 @@ export function EditRoleDialog({
|
|
|
12327
12179
|
function usersColumnsTemplate() {
|
|
12328
12180
|
return `'use client'
|
|
12329
12181
|
|
|
12330
|
-
import
|
|
12331
|
-
import {
|
|
12332
|
-
Avatar,
|
|
12333
|
-
AvatarFallback
|
|
12334
|
-
} from '@cms/components/ui/avatar'
|
|
12182
|
+
import { deleteUser } from '@cms/actions/users'
|
|
12335
12183
|
import {
|
|
12336
12184
|
AlertDialog,
|
|
12337
12185
|
AlertDialogAction,
|
|
@@ -12343,6 +12191,7 @@ import {
|
|
|
12343
12191
|
AlertDialogTitle,
|
|
12344
12192
|
AlertDialogTrigger,
|
|
12345
12193
|
} from '@cms/components/ui/alert-dialog'
|
|
12194
|
+
import { Avatar, AvatarFallback } from '@cms/components/ui/avatar'
|
|
12346
12195
|
import { Badge } from '@cms/components/ui/badge'
|
|
12347
12196
|
import { Button } from '@cms/components/ui/button'
|
|
12348
12197
|
import {
|
|
@@ -12351,14 +12200,14 @@ import {
|
|
|
12351
12200
|
DropdownMenuItem,
|
|
12352
12201
|
DropdownMenuLabel,
|
|
12353
12202
|
DropdownMenuSeparator,
|
|
12354
|
-
DropdownMenuTrigger
|
|
12203
|
+
DropdownMenuTrigger,
|
|
12355
12204
|
} from '@cms/components/ui/dropdown-menu'
|
|
12356
12205
|
import type { UserData } from '@cms/types/auth'
|
|
12357
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
12358
12206
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12207
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
12359
12208
|
import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
|
|
12209
|
+
import React from 'react'
|
|
12360
12210
|
import { toast } from 'sonner'
|
|
12361
|
-
import { deleteUser } from '@cms/actions/users'
|
|
12362
12211
|
import { EditRoleDialog } from './edit-role-dialog'
|
|
12363
12212
|
|
|
12364
12213
|
function getInitials(nameOrEmail: string): string {
|
|
@@ -12409,10 +12258,7 @@ function DeleteUserAction({
|
|
|
12409
12258
|
return (
|
|
12410
12259
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
12411
12260
|
<AlertDialogTrigger asChild>
|
|
12412
|
-
<DropdownMenuItem
|
|
12413
|
-
className="text-destructive"
|
|
12414
|
-
onSelect={(e) => e.preventDefault()}
|
|
12415
|
-
>
|
|
12261
|
+
<DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
|
|
12416
12262
|
<Trash className="size-4 mr-2" />
|
|
12417
12263
|
Delete user
|
|
12418
12264
|
</DropdownMenuItem>
|
|
@@ -12421,8 +12267,8 @@ function DeleteUserAction({
|
|
|
12421
12267
|
<AlertDialogHeader>
|
|
12422
12268
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
12423
12269
|
<AlertDialogDescription>
|
|
12424
|
-
This action cannot be undone. This will permanently delete{' '}
|
|
12425
|
-
|
|
12270
|
+
This action cannot be undone. This will permanently delete <strong>{userName}</strong>{' '}
|
|
12271
|
+
and all of their data.
|
|
12426
12272
|
</AlertDialogDescription>
|
|
12427
12273
|
</AlertDialogHeader>
|
|
12428
12274
|
<AlertDialogFooter>
|
|
@@ -12470,7 +12316,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12470
12316
|
</div>
|
|
12471
12317
|
</div>
|
|
12472
12318
|
)
|
|
12473
|
-
}
|
|
12319
|
+
},
|
|
12474
12320
|
},
|
|
12475
12321
|
{
|
|
12476
12322
|
accessorKey: 'emailVerified',
|
|
@@ -12482,7 +12328,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12482
12328
|
{verified ? 'Verified' : 'Unverified'}
|
|
12483
12329
|
</Badge>
|
|
12484
12330
|
)
|
|
12485
|
-
}
|
|
12331
|
+
},
|
|
12486
12332
|
},
|
|
12487
12333
|
{
|
|
12488
12334
|
accessorKey: 'role',
|
|
@@ -12512,7 +12358,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12512
12358
|
</Badge>
|
|
12513
12359
|
</EditRoleDialog>
|
|
12514
12360
|
)
|
|
12515
|
-
}
|
|
12361
|
+
},
|
|
12516
12362
|
},
|
|
12517
12363
|
{
|
|
12518
12364
|
accessorKey: 'createdAt',
|
|
@@ -12533,11 +12379,11 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12533
12379
|
{date.toLocaleDateString('en-US', {
|
|
12534
12380
|
month: 'short',
|
|
12535
12381
|
day: 'numeric',
|
|
12536
|
-
year: 'numeric'
|
|
12382
|
+
year: 'numeric',
|
|
12537
12383
|
})}
|
|
12538
12384
|
</div>
|
|
12539
12385
|
)
|
|
12540
|
-
}
|
|
12386
|
+
},
|
|
12541
12387
|
},
|
|
12542
12388
|
{
|
|
12543
12389
|
id: 'actions',
|
|
@@ -12556,9 +12402,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12556
12402
|
</DropdownMenuTrigger>
|
|
12557
12403
|
<DropdownMenuContent align="end">
|
|
12558
12404
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
12559
|
-
<DropdownMenuItem
|
|
12560
|
-
onClick={() => navigator.clipboard.writeText(row.original.id)}
|
|
12561
|
-
>
|
|
12405
|
+
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
|
|
12562
12406
|
Copy user ID
|
|
12563
12407
|
</DropdownMenuItem>
|
|
12564
12408
|
<DropdownMenuSeparator />
|
|
@@ -12571,8 +12415,8 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12571
12415
|
</DropdownMenu>
|
|
12572
12416
|
</div>
|
|
12573
12417
|
)
|
|
12574
|
-
}
|
|
12575
|
-
}
|
|
12418
|
+
},
|
|
12419
|
+
},
|
|
12576
12420
|
]
|
|
12577
12421
|
`;
|
|
12578
12422
|
}
|
|
@@ -12588,10 +12432,7 @@ export default function UsersPage() {
|
|
|
12588
12432
|
return (
|
|
12589
12433
|
<div className="flex flex-col">
|
|
12590
12434
|
<div className="flex items-center justify-between bg-card px-6 py-4 border-b">
|
|
12591
|
-
<PageHeader
|
|
12592
|
-
title="Users"
|
|
12593
|
-
description="Manage all CMS users"
|
|
12594
|
-
>
|
|
12435
|
+
<PageHeader title="Users">
|
|
12595
12436
|
<CreateUserDialog />
|
|
12596
12437
|
</PageHeader>
|
|
12597
12438
|
</div>
|
|
@@ -12608,6 +12449,18 @@ export default function UsersPage() {
|
|
|
12608
12449
|
function usersTableTemplate() {
|
|
12609
12450
|
return `'use client'
|
|
12610
12451
|
|
|
12452
|
+
import { authClient } from '@cms/auth/client'
|
|
12453
|
+
import { Button } from '@cms/components/ui/button'
|
|
12454
|
+
import {
|
|
12455
|
+
Table,
|
|
12456
|
+
TableBody,
|
|
12457
|
+
TableCell,
|
|
12458
|
+
TableHead,
|
|
12459
|
+
TableHeader,
|
|
12460
|
+
TableRow,
|
|
12461
|
+
} from '@cms/components/ui/table'
|
|
12462
|
+
import { useUsers } from '@cms/hooks/use-users'
|
|
12463
|
+
import type { UserData } from '@cms/types/auth'
|
|
12611
12464
|
import {
|
|
12612
12465
|
type ColumnDef,
|
|
12613
12466
|
type ColumnFiltersState,
|
|
@@ -12618,21 +12471,9 @@ import {
|
|
|
12618
12471
|
getSortedRowModel,
|
|
12619
12472
|
type SortingState,
|
|
12620
12473
|
useReactTable,
|
|
12621
|
-
type VisibilityState
|
|
12474
|
+
type VisibilityState,
|
|
12622
12475
|
} from '@tanstack/react-table'
|
|
12623
12476
|
import * as React from 'react'
|
|
12624
|
-
import { Button } from '@cms/components/ui/button'
|
|
12625
|
-
import {
|
|
12626
|
-
Table,
|
|
12627
|
-
TableBody,
|
|
12628
|
-
TableCell,
|
|
12629
|
-
TableHead,
|
|
12630
|
-
TableHeader,
|
|
12631
|
-
TableRow
|
|
12632
|
-
} from '@cms/components/ui/table'
|
|
12633
|
-
import { useUsers } from '@cms/hooks/use-users'
|
|
12634
|
-
import { authClient } from '@cms/auth/client'
|
|
12635
|
-
import type { UserData } from '@cms/types/auth'
|
|
12636
12477
|
|
|
12637
12478
|
interface UsersTableProps<TValue> {
|
|
12638
12479
|
columns: ColumnDef<UserData, TValue>[]
|
|
@@ -12662,11 +12503,11 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
|
|
|
12662
12503
|
email: session.user.email,
|
|
12663
12504
|
name: session.user.name,
|
|
12664
12505
|
image: session.user.image,
|
|
12665
|
-
role: (session.user as { role?: string }).role || 'member'
|
|
12506
|
+
role: (session.user as { role?: string }).role || 'member',
|
|
12666
12507
|
}
|
|
12667
|
-
: null
|
|
12508
|
+
: null,
|
|
12668
12509
|
},
|
|
12669
|
-
state: { sorting, columnFilters, columnVisibility }
|
|
12510
|
+
state: { sorting, columnFilters, columnVisibility },
|
|
12670
12511
|
})
|
|
12671
12512
|
|
|
12672
12513
|
return (
|
|
@@ -13606,24 +13447,29 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
|
|
|
13606
13447
|
const { execFile } = await import("child_process");
|
|
13607
13448
|
const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
|
|
13608
13449
|
const runSeed2 = (overwrite) => new Promise((resolve, reject) => {
|
|
13609
|
-
execFile(
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
}
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13450
|
+
execFile(
|
|
13451
|
+
tsxBin,
|
|
13452
|
+
[seedPath],
|
|
13453
|
+
{
|
|
13454
|
+
cwd,
|
|
13455
|
+
env: {
|
|
13456
|
+
...process.env,
|
|
13457
|
+
SEED_EMAIL: email,
|
|
13458
|
+
SEED_PASSWORD: password3,
|
|
13459
|
+
SEED_NAME: name || "Admin",
|
|
13460
|
+
...overwrite ? { SEED_OVERWRITE: "true" } : {}
|
|
13461
|
+
}
|
|
13462
|
+
},
|
|
13463
|
+
(err, stdout, stderr) => {
|
|
13464
|
+
if (err && "code" in err && err.code === 2) {
|
|
13465
|
+
resolve({ code: 2, stdout });
|
|
13466
|
+
} else if (err) {
|
|
13467
|
+
reject(new Error(stderr || err.message));
|
|
13468
|
+
} else {
|
|
13469
|
+
resolve({ code: 0, stdout });
|
|
13470
|
+
}
|
|
13625
13471
|
}
|
|
13626
|
-
|
|
13472
|
+
);
|
|
13627
13473
|
});
|
|
13628
13474
|
const spinner5 = clack.spinner();
|
|
13629
13475
|
spinner5.start("Creating admin user...");
|
|
@@ -13674,7 +13520,7 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
|
|
|
13674
13520
|
var initCommand = new Command3("init").description("Scaffold CMS into a new or existing Next.js project").argument("[name]", "Project name (creates new directory if fresh project)").option("--preset <preset>", "Starter preset: blank, blog, or full", "blog").option("-y, --yes", "Skip all prompts (accept defaults)").option(
|
|
13675
13521
|
"--database-url <url>",
|
|
13676
13522
|
"PostgreSQL database connection string (postgres:// or postgresql://)"
|
|
13677
|
-
).action(
|
|
13523
|
+
).option("--force", "Overwrite all existing CMS files (nuclear option)").action(
|
|
13678
13524
|
async (name, options) => {
|
|
13679
13525
|
p4.intro(pc2.bgCyan(pc2.black(" BetterStart CMS ")));
|
|
13680
13526
|
let cwd = process.cwd();
|
|
@@ -13689,9 +13535,35 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13689
13535
|
p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
|
|
13690
13536
|
process.exit(1);
|
|
13691
13537
|
}
|
|
13692
|
-
if (
|
|
13538
|
+
if (options.force) {
|
|
13539
|
+
const nukeDirs = ["cms", "app/(cms)"];
|
|
13540
|
+
const nukeFiles = ["cms.config.ts", "CMS.md", "drizzle.config.ts"];
|
|
13541
|
+
let nuked = 0;
|
|
13542
|
+
for (const dir of nukeDirs) {
|
|
13543
|
+
const fullPath = path37.resolve(cwd, dir);
|
|
13544
|
+
if (fs32.existsSync(fullPath)) {
|
|
13545
|
+
fs32.rmSync(fullPath, { recursive: true, force: true });
|
|
13546
|
+
nuked++;
|
|
13547
|
+
}
|
|
13548
|
+
}
|
|
13549
|
+
for (const file of nukeFiles) {
|
|
13550
|
+
const fullPath = path37.resolve(cwd, file);
|
|
13551
|
+
if (fs32.existsSync(fullPath)) {
|
|
13552
|
+
fs32.unlinkSync(fullPath);
|
|
13553
|
+
nuked++;
|
|
13554
|
+
}
|
|
13555
|
+
}
|
|
13556
|
+
if (nuked > 0) {
|
|
13557
|
+
p4.log.warn(`${pc2.yellow("Force mode:")} removed ${nuked} existing CMS paths`);
|
|
13558
|
+
}
|
|
13559
|
+
project = detectProject(cwd);
|
|
13560
|
+
} else if (project.conflicts.length > 0) {
|
|
13693
13561
|
const conflictLines = project.conflicts.map((c) => `${pc2.yellow("\u25B2")} ${c}`);
|
|
13694
|
-
conflictLines.push(
|
|
13562
|
+
conflictLines.push(
|
|
13563
|
+
"",
|
|
13564
|
+
pc2.dim("Existing files will not be overwritten."),
|
|
13565
|
+
pc2.dim(`Use ${pc2.bold("--force")} to remove existing CMS files before scaffolding.`)
|
|
13566
|
+
);
|
|
13695
13567
|
p4.note(conflictLines.join("\n"), pc2.yellow("Conflicts"));
|
|
13696
13568
|
if (!options.yes) {
|
|
13697
13569
|
const proceed = await p4.confirm({
|
|
@@ -13862,13 +13734,17 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13862
13734
|
p4.note(noteLines.join("\n"), "Scaffolded CMS");
|
|
13863
13735
|
const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
|
|
13864
13736
|
if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
|
|
13865
|
-
if (
|
|
13737
|
+
if (options.force) {
|
|
13738
|
+
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
|
|
13739
|
+
fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
|
|
13740
|
+
p4.log.success("Updated drizzle.config.ts");
|
|
13741
|
+
} else if (!options.yes) {
|
|
13866
13742
|
const overwrite = await p4.confirm({
|
|
13867
13743
|
message: "drizzle.config.ts already exists. Overwrite with latest version?",
|
|
13868
13744
|
initialValue: true
|
|
13869
13745
|
});
|
|
13870
13746
|
if (!p4.isCancel(overwrite) && overwrite) {
|
|
13871
|
-
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-
|
|
13747
|
+
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
|
|
13872
13748
|
fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
|
|
13873
13749
|
p4.log.success("Updated drizzle.config.ts");
|
|
13874
13750
|
}
|
|
@@ -13985,7 +13861,12 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13985
13861
|
seedEmail = credentials.email;
|
|
13986
13862
|
seedPassword = credentials.password;
|
|
13987
13863
|
s.start("Creating admin user");
|
|
13988
|
-
let seedResult = await runSeed(
|
|
13864
|
+
let seedResult = await runSeed(
|
|
13865
|
+
cwd,
|
|
13866
|
+
config.paths?.cms ?? "./cms",
|
|
13867
|
+
credentials.email,
|
|
13868
|
+
credentials.password
|
|
13869
|
+
);
|
|
13989
13870
|
if (seedResult.existingUser) {
|
|
13990
13871
|
s.stop(`${pc2.yellow("\u25B2")} Admin user already exists (${seedResult.existingUser})`);
|
|
13991
13872
|
const replace = await p4.confirm({
|
|
@@ -13994,7 +13875,13 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13994
13875
|
});
|
|
13995
13876
|
if (!p4.isCancel(replace) && replace) {
|
|
13996
13877
|
s.start("Replacing admin user");
|
|
13997
|
-
seedResult = await runSeed(
|
|
13878
|
+
seedResult = await runSeed(
|
|
13879
|
+
cwd,
|
|
13880
|
+
config.paths?.cms ?? "./cms",
|
|
13881
|
+
credentials.email,
|
|
13882
|
+
credentials.password,
|
|
13883
|
+
true
|
|
13884
|
+
);
|
|
13998
13885
|
} else {
|
|
13999
13886
|
seedSuccess = true;
|
|
14000
13887
|
}
|
|
@@ -14187,8 +14074,7 @@ ${stderr}`;
|
|
|
14187
14074
|
if (combined.includes("Failed to create user")) return "Auth API failed to create user";
|
|
14188
14075
|
if (combined.includes("ECONNREFUSED") || combined.includes("connection refused"))
|
|
14189
14076
|
return "Could not connect to database";
|
|
14190
|
-
if (combined.includes("BETTERSTART_DATABASE_URL"))
|
|
14191
|
-
return "Database URL is missing or invalid";
|
|
14077
|
+
if (combined.includes("BETTERSTART_DATABASE_URL")) return "Database URL is missing or invalid";
|
|
14192
14078
|
if (combined.includes("password authentication failed"))
|
|
14193
14079
|
return "Database authentication failed \u2014 check your connection string";
|
|
14194
14080
|
if (combined.includes("does not exist") && combined.includes("relation"))
|
|
@@ -14443,41 +14329,12 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
|
|
|
14443
14329
|
console.log("");
|
|
14444
14330
|
});
|
|
14445
14331
|
|
|
14446
|
-
// src/commands/update-deps.ts
|
|
14447
|
-
import path39 from "path";
|
|
14448
|
-
import * as clack2 from "@clack/prompts";
|
|
14449
|
-
import { Command as Command5 } from "commander";
|
|
14450
|
-
var updateDepsCommand = new Command5("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
|
|
14451
|
-
const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
|
|
14452
|
-
clack2.intro("BetterStart Update Dependencies");
|
|
14453
|
-
const pm = detectPackageManager(cwd);
|
|
14454
|
-
clack2.log.info(`Package manager: ${pm}`);
|
|
14455
|
-
const config = await resolveConfig(cwd);
|
|
14456
|
-
const includeEmail = config.features?.email ?? true;
|
|
14457
|
-
const s = clack2.spinner();
|
|
14458
|
-
s.start("Installing dependencies...");
|
|
14459
|
-
const result = await installDependenciesAsync({
|
|
14460
|
-
cwd,
|
|
14461
|
-
pm,
|
|
14462
|
-
includeEmail,
|
|
14463
|
-
includeBiome: false
|
|
14464
|
-
});
|
|
14465
|
-
if (result.success) {
|
|
14466
|
-
s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
|
|
14467
|
-
} else {
|
|
14468
|
-
s.stop("Dependency install failed");
|
|
14469
|
-
clack2.log.error(result.error ?? "Unknown error");
|
|
14470
|
-
process.exit(1);
|
|
14471
|
-
}
|
|
14472
|
-
clack2.outro("Dependencies updated");
|
|
14473
|
-
});
|
|
14474
|
-
|
|
14475
14332
|
// src/commands/uninstall.ts
|
|
14476
14333
|
import fs35 from "fs";
|
|
14477
|
-
import
|
|
14334
|
+
import path39 from "path";
|
|
14478
14335
|
import * as p5 from "@clack/prompts";
|
|
14336
|
+
import { Command as Command5 } from "commander";
|
|
14479
14337
|
import pc3 from "picocolors";
|
|
14480
|
-
import { Command as Command6 } from "commander";
|
|
14481
14338
|
|
|
14482
14339
|
// src/commands/uninstall-cleaners.ts
|
|
14483
14340
|
import fs34 from "fs";
|
|
@@ -14534,7 +14391,7 @@ function cleanTsconfig(tsconfigPath) {
|
|
|
14534
14391
|
}
|
|
14535
14392
|
if (removed.length === 0) return [];
|
|
14536
14393
|
if (Object.keys(paths).length === 0) {
|
|
14537
|
-
|
|
14394
|
+
compilerOptions.paths = void 0;
|
|
14538
14395
|
} else {
|
|
14539
14396
|
compilerOptions.paths = paths;
|
|
14540
14397
|
}
|
|
@@ -14588,14 +14445,14 @@ function cleanEnvFile(envPath) {
|
|
|
14588
14445
|
}
|
|
14589
14446
|
if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
|
|
14590
14447
|
const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
|
|
14591
|
-
if (nextNonEmpty
|
|
14448
|
+
if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
|
|
14592
14449
|
continue;
|
|
14593
14450
|
}
|
|
14594
14451
|
}
|
|
14595
14452
|
kept.push(line);
|
|
14596
14453
|
}
|
|
14597
14454
|
if (removed.length === 0) return [];
|
|
14598
|
-
|
|
14455
|
+
const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
14599
14456
|
if (result === "") {
|
|
14600
14457
|
fs34.unlinkSync(envPath);
|
|
14601
14458
|
} else {
|
|
@@ -14625,7 +14482,7 @@ function findMainCss2(cwd) {
|
|
|
14625
14482
|
"globals.css"
|
|
14626
14483
|
];
|
|
14627
14484
|
for (const candidate of candidates) {
|
|
14628
|
-
const filePath =
|
|
14485
|
+
const filePath = path39.join(cwd, candidate);
|
|
14629
14486
|
if (fs35.existsSync(filePath)) return filePath;
|
|
14630
14487
|
}
|
|
14631
14488
|
return void 0;
|
|
@@ -14641,11 +14498,11 @@ function isCLICreatedBiome(biomePath) {
|
|
|
14641
14498
|
}
|
|
14642
14499
|
function buildUninstallPlan(cwd) {
|
|
14643
14500
|
const steps = [];
|
|
14644
|
-
const hasSrc = fs35.existsSync(
|
|
14501
|
+
const hasSrc = fs35.existsSync(path39.join(cwd, "src"));
|
|
14645
14502
|
const appBase = hasSrc ? "src/app" : "app";
|
|
14646
14503
|
const dirs = [];
|
|
14647
|
-
const cmsDir =
|
|
14648
|
-
const cmsRouteGroup =
|
|
14504
|
+
const cmsDir = path39.join(cwd, "cms");
|
|
14505
|
+
const cmsRouteGroup = path39.join(cwd, appBase, "(cms)");
|
|
14649
14506
|
if (fs35.existsSync(cmsDir)) dirs.push("cms/");
|
|
14650
14507
|
if (fs35.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
|
|
14651
14508
|
if (dirs.length > 0) {
|
|
@@ -14663,9 +14520,9 @@ function buildUninstallPlan(cwd) {
|
|
|
14663
14520
|
const configFiles = [];
|
|
14664
14521
|
const configPaths = [];
|
|
14665
14522
|
const candidates = [
|
|
14666
|
-
["cms.config.ts",
|
|
14667
|
-
["drizzle.config.ts",
|
|
14668
|
-
["CMS.md",
|
|
14523
|
+
["cms.config.ts", path39.join(cwd, "cms.config.ts")],
|
|
14524
|
+
["drizzle.config.ts", path39.join(cwd, "drizzle.config.ts")],
|
|
14525
|
+
["CMS.md", path39.join(cwd, "CMS.md")]
|
|
14669
14526
|
];
|
|
14670
14527
|
for (const [label, fullPath] of candidates) {
|
|
14671
14528
|
if (fs35.existsSync(fullPath)) {
|
|
@@ -14673,7 +14530,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14673
14530
|
configPaths.push(fullPath);
|
|
14674
14531
|
}
|
|
14675
14532
|
}
|
|
14676
|
-
const biomePath =
|
|
14533
|
+
const biomePath = path39.join(cwd, "biome.json");
|
|
14677
14534
|
if (isCLICreatedBiome(biomePath)) {
|
|
14678
14535
|
configFiles.push("biome.json (CLI-created)");
|
|
14679
14536
|
configPaths.push(biomePath);
|
|
@@ -14691,7 +14548,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14691
14548
|
}
|
|
14692
14549
|
});
|
|
14693
14550
|
}
|
|
14694
|
-
const tsconfigPath =
|
|
14551
|
+
const tsconfigPath = path39.join(cwd, "tsconfig.json");
|
|
14695
14552
|
if (fs35.existsSync(tsconfigPath)) {
|
|
14696
14553
|
const content = fs35.readFileSync(tsconfigPath, "utf-8");
|
|
14697
14554
|
const aliasMatches = content.match(/"@cms\//g);
|
|
@@ -14713,7 +14570,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14713
14570
|
const cssContent = fs35.readFileSync(cssFile, "utf-8");
|
|
14714
14571
|
const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
|
|
14715
14572
|
if (sourceLines.length > 0) {
|
|
14716
|
-
const relCss =
|
|
14573
|
+
const relCss = path39.relative(cwd, cssFile);
|
|
14717
14574
|
steps.push({
|
|
14718
14575
|
label: `CSS @source lines (${relCss})`,
|
|
14719
14576
|
items: [`@source lines in ${relCss}`],
|
|
@@ -14725,7 +14582,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14725
14582
|
});
|
|
14726
14583
|
}
|
|
14727
14584
|
}
|
|
14728
|
-
const envPath =
|
|
14585
|
+
const envPath = path39.join(cwd, ".env.local");
|
|
14729
14586
|
if (fs35.existsSync(envPath)) {
|
|
14730
14587
|
const envContent = fs35.readFileSync(envPath, "utf-8");
|
|
14731
14588
|
const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
|
|
@@ -14743,12 +14600,12 @@ function buildUninstallPlan(cwd) {
|
|
|
14743
14600
|
}
|
|
14744
14601
|
return steps;
|
|
14745
14602
|
}
|
|
14746
|
-
var uninstallCommand = new
|
|
14747
|
-
const cwd = options.cwd ?
|
|
14603
|
+
var uninstallCommand = new Command5("uninstall").description("Remove all CMS files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action(async (options) => {
|
|
14604
|
+
const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
|
|
14748
14605
|
p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
|
|
14749
14606
|
const steps = buildUninstallPlan(cwd);
|
|
14750
14607
|
if (steps.length === 0) {
|
|
14751
|
-
p5.log.success(pc3.green("\u2713")
|
|
14608
|
+
p5.log.success(`${pc3.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
|
|
14752
14609
|
p5.outro("Done");
|
|
14753
14610
|
return;
|
|
14754
14611
|
}
|
|
@@ -14776,13 +14633,39 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
|
|
|
14776
14633
|
}
|
|
14777
14634
|
const parts = steps.map((step) => `${step.count} ${step.unit}`);
|
|
14778
14635
|
s.stop(`Removed ${parts.join(", ")}`);
|
|
14779
|
-
p5.note(
|
|
14780
|
-
pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."),
|
|
14781
|
-
"Next steps"
|
|
14782
|
-
);
|
|
14636
|
+
p5.note(pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
|
|
14783
14637
|
p5.outro("Uninstall complete");
|
|
14784
14638
|
});
|
|
14785
14639
|
|
|
14640
|
+
// src/commands/update-deps.ts
|
|
14641
|
+
import path40 from "path";
|
|
14642
|
+
import * as clack2 from "@clack/prompts";
|
|
14643
|
+
import { Command as Command6 } from "commander";
|
|
14644
|
+
var updateDepsCommand = new Command6("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
|
|
14645
|
+
const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
|
|
14646
|
+
clack2.intro("BetterStart Update Dependencies");
|
|
14647
|
+
const pm = detectPackageManager(cwd);
|
|
14648
|
+
clack2.log.info(`Package manager: ${pm}`);
|
|
14649
|
+
const config = await resolveConfig(cwd);
|
|
14650
|
+
const includeEmail = config.features?.email ?? true;
|
|
14651
|
+
const s = clack2.spinner();
|
|
14652
|
+
s.start("Installing dependencies...");
|
|
14653
|
+
const result = await installDependenciesAsync({
|
|
14654
|
+
cwd,
|
|
14655
|
+
pm,
|
|
14656
|
+
includeEmail,
|
|
14657
|
+
includeBiome: false
|
|
14658
|
+
});
|
|
14659
|
+
if (result.success) {
|
|
14660
|
+
s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
|
|
14661
|
+
} else {
|
|
14662
|
+
s.stop("Dependency install failed");
|
|
14663
|
+
clack2.log.error(result.error ?? "Unknown error");
|
|
14664
|
+
process.exit(1);
|
|
14665
|
+
}
|
|
14666
|
+
clack2.outro("Dependencies updated");
|
|
14667
|
+
});
|
|
14668
|
+
|
|
14786
14669
|
// src/commands/update-styles.ts
|
|
14787
14670
|
import fs36 from "fs";
|
|
14788
14671
|
import path41 from "path";
|