@betterstart/cli 0.1.27 → 0.1.29
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 +987 -1035
- 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
|
`;
|
|
@@ -5868,7 +5799,7 @@ import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
|
|
|
5868
5799
|
import { useRouter } from 'next/navigation'
|
|
5869
5800
|
import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
|
|
5870
5801
|
import { toast } from 'sonner'
|
|
5871
|
-
import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
5802
|
+
import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
5872
5803
|
${relHookImports ? `${relHookImports}
|
|
5873
5804
|
` : ""}${uiImports.join("\n")}
|
|
5874
5805
|
import type {
|
|
@@ -6195,7 +6126,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'${lucideIcons
|
|
|
6195
6126
|
import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
|
|
6196
6127
|
import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
|
|
6197
6128
|
import { toast } from 'sonner'
|
|
6198
|
-
import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
6129
|
+
import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
|
|
6199
6130
|
${relHookImports ? `${relHookImports}
|
|
6200
6131
|
` : ""}${uiImports.join("\n")}
|
|
6201
6132
|
import type {
|
|
@@ -6486,60 +6417,52 @@ function parseSingleItem2(str) {
|
|
|
6486
6417
|
const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
|
|
6487
6418
|
const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
|
|
6488
6419
|
const iconMatch = str.match(/icon:\s*(\w+)/);
|
|
6420
|
+
const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
|
|
6489
6421
|
if (!labelMatch || !hrefMatch) return null;
|
|
6490
6422
|
const item = {
|
|
6491
6423
|
label: labelMatch[1],
|
|
6492
6424
|
href: hrefMatch[1]
|
|
6493
6425
|
};
|
|
6494
6426
|
if (iconMatch) item.icon = iconMatch[1];
|
|
6495
|
-
|
|
6496
|
-
if (childrenMatch) {
|
|
6497
|
-
item.children = parseItemsBlock2(childrenMatch[1]);
|
|
6498
|
-
}
|
|
6427
|
+
if (groupMatch) item.group = groupMatch[1];
|
|
6499
6428
|
return item;
|
|
6500
6429
|
}
|
|
6501
6430
|
function generateNavigationCode2(items, iconImports) {
|
|
6502
6431
|
const lines = [];
|
|
6503
|
-
lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
|
|
6504
6432
|
lines.push("import type { LucideIcon } from 'lucide-react'");
|
|
6433
|
+
lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
|
|
6505
6434
|
lines.push("");
|
|
6506
6435
|
lines.push("export interface CmsNavigationItem {");
|
|
6507
6436
|
lines.push(" label: string");
|
|
6508
6437
|
lines.push(" href: string");
|
|
6509
6438
|
lines.push(" icon?: LucideIcon");
|
|
6510
|
-
lines.push("
|
|
6439
|
+
lines.push(" group?: string");
|
|
6511
6440
|
lines.push("}");
|
|
6512
6441
|
lines.push("");
|
|
6513
6442
|
lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
|
|
6514
6443
|
for (let i = 0; i < items.length; i++) {
|
|
6515
6444
|
const item = items[i];
|
|
6516
6445
|
const isLast = i === items.length - 1;
|
|
6517
|
-
appendItem2(lines, item,
|
|
6446
|
+
appendItem2(lines, item, isLast);
|
|
6518
6447
|
}
|
|
6519
6448
|
lines.push("]");
|
|
6520
6449
|
lines.push("");
|
|
6521
6450
|
return lines.join("\n");
|
|
6522
6451
|
}
|
|
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
|
-
}
|
|
6452
|
+
function appendItem2(lines, item, isLast) {
|
|
6453
|
+
lines.push(" {");
|
|
6454
|
+
lines.push(` label: '${item.label}',`);
|
|
6455
|
+
const hasMore = item.icon != null || item.group != null;
|
|
6456
|
+
lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
|
|
6457
|
+
if (item.icon && item.group) {
|
|
6458
|
+
lines.push(` icon: ${item.icon},`);
|
|
6459
|
+
lines.push(` group: '${item.group}'`);
|
|
6460
|
+
} else if (item.icon) {
|
|
6461
|
+
lines.push(` icon: ${item.icon}`);
|
|
6462
|
+
} else if (item.group) {
|
|
6463
|
+
lines.push(` group: '${item.group}'`);
|
|
6464
|
+
}
|
|
6465
|
+
lines.push(` }${isLast ? "" : ","}`);
|
|
6543
6466
|
}
|
|
6544
6467
|
function updateNavigation(schema, cwd, cmsDir, options = {}) {
|
|
6545
6468
|
const navFilePath = path15.join(cwd, cmsDir, "data", "navigation.ts");
|
|
@@ -6561,48 +6484,26 @@ function updateNavigation(schema, cwd, cmsDir, options = {}) {
|
|
|
6561
6484
|
icon: schema.icon
|
|
6562
6485
|
};
|
|
6563
6486
|
if (schema.navGroup) {
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
children: []
|
|
6571
|
-
};
|
|
6572
|
-
items.push(group);
|
|
6573
|
-
}
|
|
6574
|
-
if (!group.children) {
|
|
6575
|
-
group.children = [];
|
|
6576
|
-
}
|
|
6577
|
-
const existingChild = group.children.findIndex((c) => c.href === entityHref);
|
|
6578
|
-
if (existingChild >= 0) {
|
|
6579
|
-
if (options.force) {
|
|
6580
|
-
group.children[existingChild] = newItem;
|
|
6581
|
-
} else {
|
|
6582
|
-
return { files: [] };
|
|
6583
|
-
}
|
|
6487
|
+
newItem.group = schema.navGroup.label;
|
|
6488
|
+
}
|
|
6489
|
+
const existingIndex = items.findIndex((item) => item.href === entityHref);
|
|
6490
|
+
if (existingIndex >= 0) {
|
|
6491
|
+
if (options.force) {
|
|
6492
|
+
items[existingIndex] = newItem;
|
|
6584
6493
|
} else {
|
|
6585
|
-
|
|
6586
|
-
group.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);
|
|
6494
|
+
return { files: [] };
|
|
6590
6495
|
}
|
|
6591
6496
|
} 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
|
-
}
|
|
6497
|
+
items.push(newItem);
|
|
6602
6498
|
}
|
|
6603
6499
|
const dashboard = items.find((item) => item.href === "/cms");
|
|
6604
6500
|
const others = items.filter((item) => item.href !== "/cms");
|
|
6605
|
-
others.sort((a, b) =>
|
|
6501
|
+
others.sort((a, b) => {
|
|
6502
|
+
if (!a.group && b.group) return -1;
|
|
6503
|
+
if (a.group && !b.group) return 1;
|
|
6504
|
+
if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
|
|
6505
|
+
return a.label.localeCompare(b.label);
|
|
6506
|
+
});
|
|
6606
6507
|
items = [...dashboard ? [dashboard] : [], ...others];
|
|
6607
6508
|
if (schema.icon && !iconImports.includes(schema.icon)) {
|
|
6608
6509
|
iconImports.push(schema.icon);
|
|
@@ -6721,8 +6622,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
|
|
|
6721
6622
|
const hasCreate = schema.actions?.create ?? false;
|
|
6722
6623
|
const hasDelete = schema.actions?.delete ?? false;
|
|
6723
6624
|
const hasFilters = schema.filters && schema.filters.length > 0;
|
|
6724
|
-
const lucideIcons = ["Search"];
|
|
6725
|
-
if (hasCreate) lucideIcons.push("
|
|
6625
|
+
const lucideIcons = ["ChevronLeft", "Search", "CornerDownLeft"];
|
|
6626
|
+
if (hasCreate) lucideIcons.push("Plus");
|
|
6726
6627
|
if (hasDelete) lucideIcons.push("Trash2");
|
|
6727
6628
|
if (hasFilters) lucideIcons.push("Check", "ChevronsUpDown");
|
|
6728
6629
|
let imports = `'use client'
|
|
@@ -6730,7 +6631,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
|
|
|
6730
6631
|
import { useQueryClient } from '@tanstack/react-query'
|
|
6731
6632
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
6732
6633
|
import { ${lucideIcons.join(", ")} } from 'lucide-react'
|
|
6733
|
-
${hasCreate ? "import Link from 'next/link'\n" : ""}import {
|
|
6634
|
+
${hasCreate ? "import Link from 'next/link'\n" : ""}import { useRouter } from 'next/navigation'
|
|
6635
|
+
import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
|
|
6734
6636
|
import * as React from 'react'
|
|
6735
6637
|
import { useFormStatus } from 'react-dom'
|
|
6736
6638
|
${hasDelete ? "import { toast } from 'sonner'\n" : ""}import { PageHeader } from '@cms/components/shared/page-header'
|
|
@@ -6830,7 +6732,7 @@ ${filterLogic}` : ""}`;
|
|
|
6830
6732
|
})
|
|
6831
6733
|
}
|
|
6832
6734
|
` : "";
|
|
6833
|
-
const
|
|
6735
|
+
const _filterDropdowns = hasFilters ? schema.filters.map(
|
|
6834
6736
|
(f) => ` <Popover open={${f.field}ComboboxOpen} onOpenChange={set${toPascalCase14(f.field)}ComboboxOpen}>
|
|
6835
6737
|
<PopoverTrigger asChild>
|
|
6836
6738
|
<Button
|
|
@@ -6893,19 +6795,17 @@ ${filterLogic}` : ""}`;
|
|
|
6893
6795
|
</PopoverContent>
|
|
6894
6796
|
</Popover>`
|
|
6895
6797
|
).join("\n") : "";
|
|
6896
|
-
const searchInput =
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
<SearchButton />
|
|
6908
|
-
</form>`;
|
|
6798
|
+
const searchInput = `<form action={searchAction} className="flex items-center gap-2 relative">
|
|
6799
|
+
<Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
|
|
6800
|
+
<Input
|
|
6801
|
+
key={search}
|
|
6802
|
+
name="search"
|
|
6803
|
+
placeholder="Search ${schema.label.toLowerCase()}..."
|
|
6804
|
+
defaultValue={search}
|
|
6805
|
+
className="w-64 pl-9 bg-white rounded-lg"
|
|
6806
|
+
/>
|
|
6807
|
+
<CornerDownLeft className="text-muted-foreground/70 pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2" />
|
|
6808
|
+
</form>`;
|
|
6909
6809
|
const deleteButton = hasDelete ? ` {selectedIds.length > 0 && (
|
|
6910
6810
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
6911
6811
|
<AlertDialogTrigger asChild>
|
|
@@ -6941,7 +6841,7 @@ ${filterLogic}` : ""}`;
|
|
|
6941
6841
|
)}` : "";
|
|
6942
6842
|
const createButton = hasCreate ? ` <Button asChild>
|
|
6943
6843
|
<Link href="/cms/${schema.name}/new">
|
|
6944
|
-
<
|
|
6844
|
+
<Plus className="size-3.5 -ml-0.5" strokeWidth={2} />
|
|
6945
6845
|
Create ${singularizeLabel2(schema.label)}
|
|
6946
6846
|
</Link>
|
|
6947
6847
|
</Button>` : "";
|
|
@@ -6956,20 +6856,12 @@ ${searchButton}interface ${Plural}PageContentProps<TValue> {
|
|
|
6956
6856
|
export function ${Plural}PageContent<TValue>({
|
|
6957
6857
|
columns
|
|
6958
6858
|
}: ${Plural}PageContentProps<TValue>) {
|
|
6859
|
+
const router = useRouter()
|
|
6959
6860
|
const queryClient = useQueryClient()
|
|
6960
6861
|
${searchLogic}${deleteLogic}
|
|
6961
6862
|
return (
|
|
6962
6863
|
<>
|
|
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
|
-
|
|
6864
|
+
<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
6865
|
<main className="space-y-6 p-6">
|
|
6974
6866
|
<${Plural}Table ${tableProps} />
|
|
6975
6867
|
</main>
|
|
@@ -7018,14 +6910,12 @@ export default async function ${PageName}Page() {
|
|
|
7018
6910
|
const data = await get${Singular}()
|
|
7019
6911
|
|
|
7020
6912
|
return (
|
|
7021
|
-
|
|
7022
|
-
<
|
|
7023
|
-
|
|
7024
|
-
</div>
|
|
7025
|
-
<main className="container mx-auto max-w-5xl p-6">
|
|
6913
|
+
<>
|
|
6914
|
+
<PageHeader title="${schema.label}" />
|
|
6915
|
+
<main className="container mx-auto max-w-5xl p-6 pb-20">
|
|
7026
6916
|
<${Singular}Form initialData={data} />
|
|
7027
6917
|
</main>
|
|
7028
|
-
|
|
6918
|
+
</>
|
|
7029
6919
|
)
|
|
7030
6920
|
}
|
|
7031
6921
|
`;
|
|
@@ -7972,14 +7862,6 @@ function openBrowser(url) {
|
|
|
7972
7862
|
// src/init/prompts/features.ts
|
|
7973
7863
|
import * as p2 from "@clack/prompts";
|
|
7974
7864
|
async function promptFeatures(presetOverride) {
|
|
7975
|
-
const includeEmail = await p2.confirm({
|
|
7976
|
-
message: "Include email system? (Resend + React Email)",
|
|
7977
|
-
initialValue: true
|
|
7978
|
-
});
|
|
7979
|
-
if (p2.isCancel(includeEmail)) {
|
|
7980
|
-
p2.cancel("Setup cancelled.");
|
|
7981
|
-
process.exit(0);
|
|
7982
|
-
}
|
|
7983
7865
|
let preset;
|
|
7984
7866
|
if (presetOverride && isValidPreset(presetOverride)) {
|
|
7985
7867
|
preset = presetOverride;
|
|
@@ -7999,7 +7881,7 @@ async function promptFeatures(presetOverride) {
|
|
|
7999
7881
|
}
|
|
8000
7882
|
preset = selected;
|
|
8001
7883
|
}
|
|
8002
|
-
return { includeEmail, preset };
|
|
7884
|
+
return { includeEmail: true, preset };
|
|
8003
7885
|
}
|
|
8004
7886
|
function isValidPreset(value) {
|
|
8005
7887
|
return value === "blank" || value === "blog" || value === "full";
|
|
@@ -8067,8 +7949,8 @@ export const { GET, POST } = toNextJsHandler(auth)
|
|
|
8067
7949
|
function uploadRouteTemplate() {
|
|
8068
7950
|
return `import { PutObjectCommand } from '@aws-sdk/client-s3'
|
|
8069
7951
|
import { BUCKET_NAME, generateFilePath, getPublicUrl, getR2Client } from '@cms/lib/r2'
|
|
8070
|
-
import { validateFiles } from '@cms/utils/validation'
|
|
8071
7952
|
import type { UploadedFile } from '@cms/types'
|
|
7953
|
+
import { validateFiles } from '@cms/utils/validation'
|
|
8072
7954
|
import { type NextRequest, NextResponse } from 'next/server'
|
|
8073
7955
|
|
|
8074
7956
|
export async function POST(request: NextRequest) {
|
|
@@ -8088,27 +7970,22 @@ export async function POST(request: NextRequest) {
|
|
|
8088
7970
|
}
|
|
8089
7971
|
|
|
8090
7972
|
if (files.length === 0) {
|
|
8091
|
-
return NextResponse.json(
|
|
8092
|
-
{ success: false, error: 'No files provided' },
|
|
8093
|
-
{ status: 400 }
|
|
8094
|
-
)
|
|
7973
|
+
return NextResponse.json({ success: false, error: 'No files provided' }, { status: 400 })
|
|
8095
7974
|
}
|
|
8096
7975
|
|
|
8097
7976
|
const validation = validateFiles(files, {
|
|
8098
7977
|
maxSizeInBytes,
|
|
8099
7978
|
allowedTypes,
|
|
8100
|
-
maxFiles: 10
|
|
7979
|
+
maxFiles: 10,
|
|
8101
7980
|
})
|
|
8102
7981
|
|
|
8103
7982
|
if (!validation.valid) {
|
|
8104
7983
|
return NextResponse.json(
|
|
8105
7984
|
{
|
|
8106
7985
|
success: false,
|
|
8107
|
-
error: validation.errors
|
|
8108
|
-
.map((e) => \`\${e.filename}: \${e.error}\`)
|
|
8109
|
-
.join('; ')
|
|
7986
|
+
error: validation.errors.map((e) => \`\${e.filename}: \${e.error}\`).join('; '),
|
|
8110
7987
|
},
|
|
8111
|
-
{ status: 400 }
|
|
7988
|
+
{ status: 400 },
|
|
8112
7989
|
)
|
|
8113
7990
|
}
|
|
8114
7991
|
|
|
@@ -8124,7 +8001,7 @@ export async function POST(request: NextRequest) {
|
|
|
8124
8001
|
Key: key,
|
|
8125
8002
|
Body: buffer,
|
|
8126
8003
|
ContentType: file.type,
|
|
8127
|
-
ContentLength: file.size
|
|
8004
|
+
ContentLength: file.size,
|
|
8128
8005
|
})
|
|
8129
8006
|
|
|
8130
8007
|
await getR2Client().send(command)
|
|
@@ -8134,18 +8011,14 @@ export async function POST(request: NextRequest) {
|
|
|
8134
8011
|
url: getPublicUrl(key),
|
|
8135
8012
|
filename: file.name,
|
|
8136
8013
|
size: file.size,
|
|
8137
|
-
contentType: file.type
|
|
8014
|
+
contentType: file.type,
|
|
8138
8015
|
})
|
|
8139
8016
|
}
|
|
8140
8017
|
|
|
8141
8018
|
return NextResponse.json({ success: true, files: uploadedFiles })
|
|
8142
8019
|
} catch (error) {
|
|
8143
|
-
const message =
|
|
8144
|
-
|
|
8145
|
-
return NextResponse.json(
|
|
8146
|
-
{ success: false, error: message },
|
|
8147
|
-
{ status: 500 }
|
|
8148
|
-
)
|
|
8020
|
+
const message = error instanceof Error ? error.message : 'Failed to upload files'
|
|
8021
|
+
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
|
8149
8022
|
}
|
|
8150
8023
|
}
|
|
8151
8024
|
`;
|
|
@@ -8225,7 +8098,9 @@ function authClientTemplate() {
|
|
|
8225
8098
|
import { createAuthClient } from 'better-auth/react'
|
|
8226
8099
|
|
|
8227
8100
|
export const authClient = createAuthClient({
|
|
8228
|
-
baseURL:
|
|
8101
|
+
baseURL:
|
|
8102
|
+
process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
|
|
8103
|
+
(typeof window !== 'undefined' ? window.location.origin : ''),
|
|
8229
8104
|
basePath: '/api/cms/auth',
|
|
8230
8105
|
})
|
|
8231
8106
|
|
|
@@ -8246,10 +8121,7 @@ export enum UserRole {
|
|
|
8246
8121
|
}
|
|
8247
8122
|
|
|
8248
8123
|
export function isUserRole(value: unknown): value is UserRole {
|
|
8249
|
-
return (
|
|
8250
|
-
typeof value === 'string' &&
|
|
8251
|
-
Object.values(UserRole).includes(value as UserRole)
|
|
8252
|
-
)
|
|
8124
|
+
return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
|
|
8253
8125
|
}
|
|
8254
8126
|
|
|
8255
8127
|
/**
|
|
@@ -8704,184 +8576,121 @@ function hasEnvBetterstartVars(cwd) {
|
|
|
8704
8576
|
// src/init/templates/components/cms-globals.ts
|
|
8705
8577
|
function cmsGlobalsCssTemplate() {
|
|
8706
8578
|
return `@import "tailwindcss";
|
|
8579
|
+
@import "tw-animate-css";
|
|
8580
|
+
@import "shadcn/tailwind.css";
|
|
8707
8581
|
|
|
8708
8582
|
@custom-variant dark (&:is(.dark *));
|
|
8709
8583
|
|
|
8710
|
-
:root {
|
|
8711
|
-
--background: oklch(0.985 0 0);
|
|
8712
|
-
--foreground: oklch(0 0 0);
|
|
8713
|
-
--card: oklch(1 0 0);
|
|
8714
|
-
--card-foreground: oklch(0 0 0);
|
|
8715
|
-
--popover: oklch(0.99 0 0);
|
|
8716
|
-
--popover-foreground: oklch(0 0 0);
|
|
8717
|
-
--primary: oklch(0 0 0);
|
|
8718
|
-
--primary-foreground: oklch(1 0 0);
|
|
8719
|
-
--secondary: oklch(0.97 0 0);
|
|
8720
|
-
--secondary-foreground: oklch(0 0 0);
|
|
8721
|
-
--muted: oklch(0.97 0 0);
|
|
8722
|
-
--muted-foreground: oklch(0.44 0 0);
|
|
8723
|
-
--accent: oklch(0.97 0 0);
|
|
8724
|
-
--accent-foreground: oklch(0 0 0);
|
|
8725
|
-
--destructive: oklch(0.63 0.19 23.03);
|
|
8726
|
-
--destructive-foreground: oklch(1 0 0);
|
|
8727
|
-
--border: oklch(0.92 0 0);
|
|
8728
|
-
--input: oklch(0.99 0 0);
|
|
8729
|
-
--ring: oklch(0 0 0);
|
|
8730
|
-
--chart-1: oklch(0.81 0.17 75.35);
|
|
8731
|
-
--chart-2: oklch(0.55 0.22 264.53);
|
|
8732
|
-
--chart-3: oklch(0.72 0 0);
|
|
8733
|
-
--chart-4: oklch(0.92 0 0);
|
|
8734
|
-
--chart-5: oklch(0.56 0 0);
|
|
8735
|
-
--sidebar: oklch(1 0 0);
|
|
8736
|
-
--sidebar-foreground: oklch(0 0 0);
|
|
8737
|
-
--sidebar-primary: oklch(0 0 0);
|
|
8738
|
-
--sidebar-primary-foreground: oklch(1 0 0);
|
|
8739
|
-
--sidebar-accent: oklch(0.94 0 0);
|
|
8740
|
-
--sidebar-accent-foreground: oklch(0 0 0);
|
|
8741
|
-
--sidebar-border: oklch(0.94 0 0);
|
|
8742
|
-
--sidebar-ring: oklch(0 0 0);
|
|
8743
|
-
--font-sans: Geist, sans-serif;
|
|
8744
|
-
--font-serif: Georgia, serif;
|
|
8745
|
-
--font-mono: Geist Mono, monospace;
|
|
8746
|
-
--radius: 0.4rem;
|
|
8747
|
-
--shadow-x: 0px;
|
|
8748
|
-
--shadow-y: 1px;
|
|
8749
|
-
--shadow-blur: 3px;
|
|
8750
|
-
--shadow-spread: 0px;
|
|
8751
|
-
--shadow-opacity: 0.02;
|
|
8752
|
-
--shadow-color: hsl(0 0% 0%);
|
|
8753
|
-
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8754
|
-
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8755
|
-
--shadow-sm:
|
|
8756
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8757
|
-
--shadow:
|
|
8758
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8759
|
-
--shadow-md:
|
|
8760
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
|
|
8761
|
-
--shadow-lg:
|
|
8762
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
|
|
8763
|
-
--shadow-xl:
|
|
8764
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
|
|
8765
|
-
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
|
|
8766
|
-
--tracking-normal: 0em;
|
|
8767
|
-
--spacing: 0.25rem;
|
|
8768
|
-
}
|
|
8769
|
-
|
|
8770
|
-
.dark {
|
|
8771
|
-
--background: oklch(0 0 0);
|
|
8772
|
-
--foreground: oklch(1 0 0);
|
|
8773
|
-
--card: oklch(0.14 0 0);
|
|
8774
|
-
--card-foreground: oklch(1 0 0);
|
|
8775
|
-
--popover: oklch(0.18 0 0);
|
|
8776
|
-
--popover-foreground: oklch(1 0 0);
|
|
8777
|
-
--primary: oklch(1 0 0);
|
|
8778
|
-
--primary-foreground: oklch(0 0 0);
|
|
8779
|
-
--secondary: oklch(0.25 0 0);
|
|
8780
|
-
--secondary-foreground: oklch(1 0 0);
|
|
8781
|
-
--muted: oklch(0.23 0 0);
|
|
8782
|
-
--muted-foreground: oklch(0.72 0 0);
|
|
8783
|
-
--accent: oklch(0.32 0 0);
|
|
8784
|
-
--accent-foreground: oklch(1 0 0);
|
|
8785
|
-
--destructive: oklch(0.69 0.2 23.91);
|
|
8786
|
-
--destructive-foreground: oklch(0 0 0);
|
|
8787
|
-
--border: oklch(0.26 0 0);
|
|
8788
|
-
--input: oklch(0.32 0 0);
|
|
8789
|
-
--ring: oklch(0.72 0 0);
|
|
8790
|
-
--chart-1: oklch(0.81 0.17 75.35);
|
|
8791
|
-
--chart-2: oklch(0.58 0.21 260.84);
|
|
8792
|
-
--chart-3: oklch(0.56 0 0);
|
|
8793
|
-
--chart-4: oklch(0.44 0 0);
|
|
8794
|
-
--chart-5: oklch(0.92 0 0);
|
|
8795
|
-
--sidebar: oklch(0.18 0 0);
|
|
8796
|
-
--sidebar-foreground: oklch(1 0 0);
|
|
8797
|
-
--sidebar-primary: oklch(1 0 0);
|
|
8798
|
-
--sidebar-primary-foreground: oklch(0 0 0);
|
|
8799
|
-
--sidebar-accent: oklch(0.32 0 0);
|
|
8800
|
-
--sidebar-accent-foreground: oklch(1 0 0);
|
|
8801
|
-
--sidebar-border: oklch(0.32 0 0);
|
|
8802
|
-
--sidebar-ring: oklch(0.72 0 0);
|
|
8803
|
-
--font-sans: Geist, sans-serif;
|
|
8804
|
-
--font-serif: Georgia, serif;
|
|
8805
|
-
--font-mono: Geist Mono, monospace;
|
|
8806
|
-
--radius: 0.4rem;
|
|
8807
|
-
--shadow-x: 0px;
|
|
8808
|
-
--shadow-y: 1px;
|
|
8809
|
-
--shadow-blur: 3px;
|
|
8810
|
-
--shadow-spread: 0px;
|
|
8811
|
-
--shadow-opacity: 0.02;
|
|
8812
|
-
--shadow-color: hsl(0 0% 0%);
|
|
8813
|
-
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8814
|
-
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8815
|
-
--shadow-sm:
|
|
8816
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8817
|
-
--shadow:
|
|
8818
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8819
|
-
--shadow-md:
|
|
8820
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
|
|
8821
|
-
--shadow-lg:
|
|
8822
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
|
|
8823
|
-
--shadow-xl:
|
|
8824
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
|
|
8825
|
-
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
|
|
8826
|
-
}
|
|
8827
|
-
|
|
8828
|
-
.cms-root {
|
|
8829
|
-
--font-sans: var(--font-geist-sans, sans-serif);
|
|
8830
|
-
--font-mono: var(--font-geist-mono, monospace);
|
|
8831
|
-
font-family: var(--font-sans);
|
|
8832
|
-
}
|
|
8833
|
-
|
|
8834
8584
|
@theme inline {
|
|
8835
8585
|
--color-background: var(--background);
|
|
8836
8586
|
--color-foreground: var(--foreground);
|
|
8837
|
-
--color-card: var(--card);
|
|
8838
|
-
--color-card-foreground: var(--card-foreground);
|
|
8839
|
-
--color-popover: var(--popover);
|
|
8840
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
8841
|
-
--color-primary: var(--primary);
|
|
8842
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
8843
|
-
--color-secondary: var(--secondary);
|
|
8844
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
8845
|
-
--color-muted: var(--muted);
|
|
8846
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
8847
|
-
--color-accent: var(--accent);
|
|
8848
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
8849
|
-
--color-destructive: var(--destructive);
|
|
8850
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
8851
|
-
--color-border: var(--border);
|
|
8852
|
-
--color-input: var(--input);
|
|
8853
|
-
--color-ring: var(--ring);
|
|
8854
|
-
--color-chart-1: var(--chart-1);
|
|
8855
|
-
--color-chart-2: var(--chart-2);
|
|
8856
|
-
--color-chart-3: var(--chart-3);
|
|
8857
|
-
--color-chart-4: var(--chart-4);
|
|
8858
|
-
--color-chart-5: var(--chart-5);
|
|
8859
|
-
--color-sidebar: var(--sidebar);
|
|
8860
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
8861
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
8862
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
8863
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
8864
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
8865
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
8866
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
8867
|
-
|
|
8868
8587
|
--font-sans: var(--font-sans);
|
|
8869
|
-
--font-mono: var(--font-mono);
|
|
8870
|
-
--
|
|
8871
|
-
|
|
8588
|
+
--font-mono: var(--font-geist-mono);
|
|
8589
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
8590
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
8591
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
8592
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
8593
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
8594
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
8595
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
8596
|
+
--color-sidebar: var(--sidebar);
|
|
8597
|
+
--color-chart-5: var(--chart-5);
|
|
8598
|
+
--color-chart-4: var(--chart-4);
|
|
8599
|
+
--color-chart-3: var(--chart-3);
|
|
8600
|
+
--color-chart-2: var(--chart-2);
|
|
8601
|
+
--color-chart-1: var(--chart-1);
|
|
8602
|
+
--color-ring: var(--ring);
|
|
8603
|
+
--color-input: var(--input);
|
|
8604
|
+
--color-border: var(--border);
|
|
8605
|
+
--color-destructive: var(--destructive);
|
|
8606
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
8607
|
+
--color-accent: var(--accent);
|
|
8608
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
8609
|
+
--color-muted: var(--muted);
|
|
8610
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
8611
|
+
--color-secondary: var(--secondary);
|
|
8612
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
8613
|
+
--color-primary: var(--primary);
|
|
8614
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
8615
|
+
--color-popover: var(--popover);
|
|
8616
|
+
--color-card-foreground: var(--card-foreground);
|
|
8617
|
+
--color-card: var(--card);
|
|
8872
8618
|
--radius-sm: calc(var(--radius) - 4px);
|
|
8873
8619
|
--radius-md: calc(var(--radius) - 2px);
|
|
8874
8620
|
--radius-lg: var(--radius);
|
|
8875
8621
|
--radius-xl: calc(var(--radius) + 4px);
|
|
8622
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
8623
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
8624
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
8625
|
+
}
|
|
8626
|
+
|
|
8627
|
+
:root {
|
|
8628
|
+
--background: oklch(1 0 0);
|
|
8629
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
8630
|
+
--card: oklch(1 0 0);
|
|
8631
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
8632
|
+
--popover: oklch(1 0 0);
|
|
8633
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
8634
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
8635
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
8636
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
8637
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
8638
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
8639
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
8640
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
8641
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
8642
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
8643
|
+
--border: oklch(0.92 0.004 286.32);
|
|
8644
|
+
--input: oklch(0.92 0.004 286.32);
|
|
8645
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
8646
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
8647
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
8648
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
8649
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
8650
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
8651
|
+
--radius: 0.625rem;
|
|
8652
|
+
--sidebar: oklch(0.985 0 0);
|
|
8653
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
8654
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
8655
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
8656
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
8657
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
8658
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
8659
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
8660
|
+
}
|
|
8876
8661
|
|
|
8877
|
-
|
|
8878
|
-
--
|
|
8879
|
-
--
|
|
8880
|
-
--
|
|
8881
|
-
--
|
|
8882
|
-
--
|
|
8883
|
-
--
|
|
8884
|
-
--
|
|
8662
|
+
.dark {
|
|
8663
|
+
--background: oklch(0.141 0.005 285.823);
|
|
8664
|
+
--foreground: oklch(0.985 0 0);
|
|
8665
|
+
--card: oklch(0.21 0.006 285.885);
|
|
8666
|
+
--card-foreground: oklch(0.985 0 0);
|
|
8667
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
8668
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
8669
|
+
--primary: oklch(0.92 0.004 286.32);
|
|
8670
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
8671
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
8672
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
8673
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
8674
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
8675
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
8676
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
8677
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
8678
|
+
--border: oklch(1 0 0 / 10%);
|
|
8679
|
+
--input: oklch(1 0 0 / 15%);
|
|
8680
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
8681
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
8682
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
8683
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
8684
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
8685
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
8686
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
8687
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
8688
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
8689
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
8690
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
8691
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
8692
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
8693
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
8885
8694
|
}
|
|
8886
8695
|
|
|
8887
8696
|
@layer base {
|
|
@@ -8889,7 +8698,7 @@ function cmsGlobalsCssTemplate() {
|
|
|
8889
8698
|
@apply border-border outline-ring/50;
|
|
8890
8699
|
}
|
|
8891
8700
|
body {
|
|
8892
|
-
@apply bg-background text-foreground
|
|
8701
|
+
@apply bg-background text-foreground;
|
|
8893
8702
|
}
|
|
8894
8703
|
}
|
|
8895
8704
|
|
|
@@ -8922,6 +8731,14 @@ function cmsGlobalsCssTemplate() {
|
|
|
8922
8731
|
function dataTableTemplate() {
|
|
8923
8732
|
return `'use client'
|
|
8924
8733
|
|
|
8734
|
+
import {
|
|
8735
|
+
Table,
|
|
8736
|
+
TableBody,
|
|
8737
|
+
TableCell,
|
|
8738
|
+
TableHead,
|
|
8739
|
+
TableHeader,
|
|
8740
|
+
TableRow,
|
|
8741
|
+
} from '@cms/components/ui/table'
|
|
8925
8742
|
import {
|
|
8926
8743
|
type ColumnDef,
|
|
8927
8744
|
type ColumnFiltersState,
|
|
@@ -8931,20 +8748,11 @@ import {
|
|
|
8931
8748
|
getPaginationRowModel,
|
|
8932
8749
|
getSortedRowModel,
|
|
8933
8750
|
type SortingState,
|
|
8934
|
-
type Table as TanstackTable,
|
|
8935
8751
|
useReactTable,
|
|
8936
|
-
type VisibilityState
|
|
8752
|
+
type VisibilityState,
|
|
8937
8753
|
} from '@tanstack/react-table'
|
|
8938
8754
|
import { parseAsInteger, useQueryState } from 'nuqs'
|
|
8939
8755
|
import * as React from 'react'
|
|
8940
|
-
import {
|
|
8941
|
-
Table,
|
|
8942
|
-
TableBody,
|
|
8943
|
-
TableCell,
|
|
8944
|
-
TableHead,
|
|
8945
|
-
TableHeader,
|
|
8946
|
-
TableRow
|
|
8947
|
-
} from '@cms/components/ui/table'
|
|
8948
8756
|
import { DataTablePagination } from './data-table-pagination'
|
|
8949
8757
|
|
|
8950
8758
|
interface DataTableProps<TData, TValue> {
|
|
@@ -8970,7 +8778,7 @@ export function DataTable<TData, TValue>({
|
|
|
8970
8778
|
selectedIds,
|
|
8971
8779
|
onSelectedIdsChange,
|
|
8972
8780
|
meta,
|
|
8973
|
-
getId = (row) => (row as { id: number }).id
|
|
8781
|
+
getId = (row) => (row as { id: number }).id,
|
|
8974
8782
|
}: DataTableProps<TData, TValue>) {
|
|
8975
8783
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
8976
8784
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
|
@@ -8993,27 +8801,38 @@ export function DataTable<TData, TValue>({
|
|
|
8993
8801
|
}, [selectedIds, data, getId])
|
|
8994
8802
|
|
|
8995
8803
|
const handleRowSelectionChange = React.useCallback(
|
|
8996
|
-
(
|
|
8804
|
+
(
|
|
8805
|
+
updater:
|
|
8806
|
+
| Record<string, boolean>
|
|
8807
|
+
| ((old: Record<string, boolean>) => Record<string, boolean>),
|
|
8808
|
+
) => {
|
|
8997
8809
|
if (!onSelectedIdsChange) return
|
|
8998
8810
|
const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater
|
|
8999
8811
|
const newIds = Object.keys(newSelection)
|
|
9000
8812
|
.filter((key) => newSelection[key])
|
|
9001
|
-
.map((key) => getId(data[Number.parseInt(key)]))
|
|
8813
|
+
.map((key) => getId(data[Number.parseInt(key, 10)]))
|
|
9002
8814
|
.filter(Boolean)
|
|
9003
8815
|
onSelectedIdsChange(newIds)
|
|
9004
8816
|
},
|
|
9005
|
-
[data, rowSelection, onSelectedIdsChange, getId]
|
|
8817
|
+
[data, rowSelection, onSelectedIdsChange, getId],
|
|
9006
8818
|
)
|
|
9007
8819
|
|
|
9008
8820
|
const handlePaginationChange = React.useCallback(
|
|
9009
|
-
(
|
|
8821
|
+
(
|
|
8822
|
+
updater:
|
|
8823
|
+
| { pageIndex: number; pageSize: number }
|
|
8824
|
+
| ((old: { pageIndex: number; pageSize: number }) => {
|
|
8825
|
+
pageIndex: number
|
|
8826
|
+
pageSize: number
|
|
8827
|
+
}),
|
|
8828
|
+
) => {
|
|
9010
8829
|
const current = { pageIndex, pageSize: effectivePageSize }
|
|
9011
8830
|
const next = typeof updater === 'function' ? updater(current) : updater
|
|
9012
8831
|
React.startTransition(() => {
|
|
9013
8832
|
setPageIndex(next.pageIndex)
|
|
9014
8833
|
})
|
|
9015
8834
|
},
|
|
9016
|
-
[pageIndex, effectivePageSize, setPageIndex]
|
|
8835
|
+
[pageIndex, effectivePageSize, setPageIndex],
|
|
9017
8836
|
)
|
|
9018
8837
|
|
|
9019
8838
|
const table = useReactTable({
|
|
@@ -9036,9 +8855,9 @@ export function DataTable<TData, TValue>({
|
|
|
9036
8855
|
rowSelection,
|
|
9037
8856
|
pagination: {
|
|
9038
8857
|
pageIndex,
|
|
9039
|
-
pageSize: effectivePageSize
|
|
9040
|
-
}
|
|
9041
|
-
}
|
|
8858
|
+
pageSize: effectivePageSize,
|
|
8859
|
+
},
|
|
8860
|
+
},
|
|
9042
8861
|
})
|
|
9043
8862
|
|
|
9044
8863
|
return (
|
|
@@ -9105,23 +8924,23 @@ export type { DataTableProps }
|
|
|
9105
8924
|
function dataTablePaginationTemplate() {
|
|
9106
8925
|
return `'use client'
|
|
9107
8926
|
|
|
9108
|
-
import type { Table } from '@tanstack/react-table'
|
|
9109
|
-
import * as React from 'react'
|
|
9110
8927
|
import { Button } from '@cms/components/ui/button'
|
|
9111
8928
|
import {
|
|
9112
8929
|
Select,
|
|
9113
8930
|
SelectContent,
|
|
9114
8931
|
SelectItem,
|
|
9115
8932
|
SelectTrigger,
|
|
9116
|
-
SelectValue
|
|
8933
|
+
SelectValue,
|
|
9117
8934
|
} from '@cms/components/ui/select'
|
|
8935
|
+
import type { Table } from '@tanstack/react-table'
|
|
8936
|
+
import * as React from 'react'
|
|
9118
8937
|
|
|
9119
8938
|
const PAGE_SIZE_OPTIONS = [
|
|
9120
8939
|
{ value: '10', label: '10' },
|
|
9121
8940
|
{ value: '20', label: '20' },
|
|
9122
8941
|
{ value: '50', label: '50' },
|
|
9123
8942
|
{ value: '100', label: '100' },
|
|
9124
|
-
{ value: 'all', label: 'All' }
|
|
8943
|
+
{ value: 'all', label: 'All' },
|
|
9125
8944
|
]
|
|
9126
8945
|
|
|
9127
8946
|
interface DataTablePaginationProps<TData> {
|
|
@@ -9133,7 +8952,7 @@ interface DataTablePaginationProps<TData> {
|
|
|
9133
8952
|
export function DataTablePagination<TData>({
|
|
9134
8953
|
table,
|
|
9135
8954
|
pageSize,
|
|
9136
|
-
setPageSize
|
|
8955
|
+
setPageSize,
|
|
9137
8956
|
}: DataTablePaginationProps<TData>) {
|
|
9138
8957
|
const handlePageSizeChange = React.useCallback(
|
|
9139
8958
|
(value: string) => {
|
|
@@ -9146,7 +8965,7 @@ export function DataTablePagination<TData>({
|
|
|
9146
8965
|
table.setPageIndex(0)
|
|
9147
8966
|
})
|
|
9148
8967
|
},
|
|
9149
|
-
[setPageSize, table]
|
|
8968
|
+
[setPageSize, table],
|
|
9150
8969
|
)
|
|
9151
8970
|
|
|
9152
8971
|
return (
|
|
@@ -9200,11 +9019,11 @@ export function DataTablePagination<TData>({
|
|
|
9200
9019
|
function dataTableToolbarTemplate() {
|
|
9201
9020
|
return `'use client'
|
|
9202
9021
|
|
|
9203
|
-
import { ArrowUpDown, Save, Search } from 'lucide-react'
|
|
9204
|
-
import * as React from 'react'
|
|
9205
|
-
import { useFormStatus } from 'react-dom'
|
|
9206
9022
|
import { Button } from '@cms/components/ui/button'
|
|
9207
9023
|
import { Input } from '@cms/components/ui/input'
|
|
9024
|
+
import { ArrowUpDown, Save, Search } from 'lucide-react'
|
|
9025
|
+
import type * as React from 'react'
|
|
9026
|
+
import { useFormStatus } from 'react-dom'
|
|
9208
9027
|
|
|
9209
9028
|
function SearchButton() {
|
|
9210
9029
|
const { pending } = useFormStatus()
|
|
@@ -9226,7 +9045,7 @@ export function DataTableToolbar({
|
|
|
9226
9045
|
search,
|
|
9227
9046
|
onSearch,
|
|
9228
9047
|
searchPlaceholder = 'Search...',
|
|
9229
|
-
children
|
|
9048
|
+
children,
|
|
9230
9049
|
}: DataTableToolbarProps) {
|
|
9231
9050
|
return (
|
|
9232
9051
|
<div className="flex items-center gap-2">
|
|
@@ -9263,7 +9082,7 @@ export function ReorderControls({
|
|
|
9263
9082
|
onSave,
|
|
9264
9083
|
onCancel,
|
|
9265
9084
|
hasChanges,
|
|
9266
|
-
isSaving
|
|
9085
|
+
isSaving,
|
|
9267
9086
|
}: ReorderControlsProps) {
|
|
9268
9087
|
return (
|
|
9269
9088
|
<div className="flex items-center gap-2">
|
|
@@ -9278,26 +9097,14 @@ export function ReorderControls({
|
|
|
9278
9097
|
</Button>
|
|
9279
9098
|
{reorderMode && (
|
|
9280
9099
|
<>
|
|
9281
|
-
<Button
|
|
9282
|
-
variant="default"
|
|
9283
|
-
size="sm"
|
|
9284
|
-
onClick={onSave}
|
|
9285
|
-
disabled={!hasChanges || isSaving}
|
|
9286
|
-
>
|
|
9100
|
+
<Button variant="default" size="sm" onClick={onSave} disabled={!hasChanges || isSaving}>
|
|
9287
9101
|
<Save className="size-4 mr-1" />
|
|
9288
9102
|
{isSaving ? 'Saving...' : 'Save'}
|
|
9289
9103
|
</Button>
|
|
9290
|
-
<Button
|
|
9291
|
-
variant="outline"
|
|
9292
|
-
size="sm"
|
|
9293
|
-
onClick={onCancel}
|
|
9294
|
-
disabled={isSaving}
|
|
9295
|
-
>
|
|
9104
|
+
<Button variant="outline" size="sm" onClick={onCancel} disabled={isSaving}>
|
|
9296
9105
|
Cancel
|
|
9297
9106
|
</Button>
|
|
9298
|
-
{hasChanges &&
|
|
9299
|
-
<span className="text-sm text-muted-foreground">Unsaved changes</span>
|
|
9300
|
-
)}
|
|
9107
|
+
{hasChanges && <span className="text-sm text-muted-foreground">Unsaved changes</span>}
|
|
9301
9108
|
</>
|
|
9302
9109
|
)}
|
|
9303
9110
|
</div>
|
|
@@ -9310,7 +9117,6 @@ export function ReorderControls({
|
|
|
9310
9117
|
function cmsHeaderTemplate() {
|
|
9311
9118
|
return `'use client'
|
|
9312
9119
|
|
|
9313
|
-
import { CmsSearch } from '@cms/components/layout/cms-search'
|
|
9314
9120
|
import { Button } from '@cms/components/ui/button'
|
|
9315
9121
|
import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
|
|
9316
9122
|
import { useTheme } from '@cms/hooks/use-cms-theme'
|
|
@@ -9321,11 +9127,10 @@ export function CmsHeader() {
|
|
|
9321
9127
|
const { state } = useSidebar()
|
|
9322
9128
|
|
|
9323
9129
|
return (
|
|
9324
|
-
<header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50
|
|
9130
|
+
<header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50">
|
|
9325
9131
|
<div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
|
|
9326
9132
|
<div className="flex items-center gap-2 w-full">
|
|
9327
9133
|
{state === 'collapsed' && <SidebarTrigger />}
|
|
9328
|
-
<CmsSearch />
|
|
9329
9134
|
</div>
|
|
9330
9135
|
<div className="flex items-center gap-2 ml-auto">
|
|
9331
9136
|
<Button
|
|
@@ -9345,17 +9150,47 @@ export function CmsHeader() {
|
|
|
9345
9150
|
`;
|
|
9346
9151
|
}
|
|
9347
9152
|
|
|
9348
|
-
// src/init/templates/components/layout/cms-
|
|
9349
|
-
function
|
|
9153
|
+
// src/init/templates/components/layout/cms-nav-link.ts
|
|
9154
|
+
function cmsNavLinkTemplate() {
|
|
9350
9155
|
return `'use client'
|
|
9351
9156
|
|
|
9352
|
-
import {
|
|
9353
|
-
import
|
|
9354
|
-
import {
|
|
9355
|
-
import { Toaster } from '@cms/components/ui/sonner'
|
|
9356
|
-
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
|
9157
|
+
import { SidebarMenuButton } from '@cms/components/ui/sidebar'
|
|
9158
|
+
import Link from 'next/link'
|
|
9159
|
+
import { usePathname } from 'next/navigation'
|
|
9357
9160
|
|
|
9358
|
-
export function
|
|
9161
|
+
export function CmsNavLink({
|
|
9162
|
+
href,
|
|
9163
|
+
children,
|
|
9164
|
+
}: {
|
|
9165
|
+
href: string
|
|
9166
|
+
children: React.ReactNode
|
|
9167
|
+
}) {
|
|
9168
|
+
const pathname = usePathname()
|
|
9169
|
+
const isActive =
|
|
9170
|
+
href === '/cms'
|
|
9171
|
+
? pathname === '/cms'
|
|
9172
|
+
: pathname === href || pathname.startsWith(href + '/')
|
|
9173
|
+
|
|
9174
|
+
return (
|
|
9175
|
+
<SidebarMenuButton asChild isActive={isActive}>
|
|
9176
|
+
<Link href={href}>{children}</Link>
|
|
9177
|
+
</SidebarMenuButton>
|
|
9178
|
+
)
|
|
9179
|
+
}
|
|
9180
|
+
`;
|
|
9181
|
+
}
|
|
9182
|
+
|
|
9183
|
+
// src/init/templates/components/layout/cms-providers.ts
|
|
9184
|
+
function cmsProvidersTemplate() {
|
|
9185
|
+
return `'use client'
|
|
9186
|
+
|
|
9187
|
+
import { Toaster } from '@cms/components/ui/sonner'
|
|
9188
|
+
import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
|
|
9189
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
9190
|
+
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
|
9191
|
+
import { useState } from 'react'
|
|
9192
|
+
|
|
9193
|
+
export function CmsProviders({ children }: { children: React.ReactNode }) {
|
|
9359
9194
|
const [queryClient] = useState(
|
|
9360
9195
|
() =>
|
|
9361
9196
|
new QueryClient({
|
|
@@ -9393,15 +9228,15 @@ export const CmsSearch = () => {
|
|
|
9393
9228
|
<div className="flex items-center gap-2 relative w-full max-w-[240px]">
|
|
9394
9229
|
<Button
|
|
9395
9230
|
variant="outline"
|
|
9396
|
-
className="w-full text-left items-center pr-1! rounded-
|
|
9397
|
-
size="
|
|
9231
|
+
className="w-full text-left items-center pr-1.5! py-0 rounded-lg bg-white"
|
|
9232
|
+
size="lg"
|
|
9398
9233
|
>
|
|
9399
|
-
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground"
|
|
9400
|
-
<span className="w-full font-
|
|
9401
|
-
|
|
9234
|
+
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground/70" />
|
|
9235
|
+
<span className="w-full font-normal text-sm text-muted-foreground/70 [text-box-trim:trim-both]">
|
|
9236
|
+
Quick search...
|
|
9402
9237
|
</span>
|
|
9403
9238
|
<div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
|
|
9404
|
-
<Command className="size-3! text-muted-foreground"
|
|
9239
|
+
<Command className="size-3! text-muted-foreground" />
|
|
9405
9240
|
<span className="font-mono text-xs font-medium">K</span>
|
|
9406
9241
|
</div>
|
|
9407
9242
|
</Button>
|
|
@@ -9415,123 +9250,110 @@ CmsSearch.displayName = 'CmsSearch'
|
|
|
9415
9250
|
|
|
9416
9251
|
// src/init/templates/components/layout/cms-sidebar.ts
|
|
9417
9252
|
function cmsSidebarTemplate() {
|
|
9418
|
-
return `import {
|
|
9419
|
-
import { getSession } from '@cms/auth/middleware'
|
|
9253
|
+
return `import { getSession } from '@cms/auth/middleware'
|
|
9420
9254
|
import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
|
|
9421
|
-
import {
|
|
9422
|
-
Collapsible,
|
|
9423
|
-
CollapsibleContent,
|
|
9424
|
-
CollapsibleTrigger,
|
|
9425
|
-
} from '@cms/components/ui/collapsible'
|
|
9426
9255
|
import {
|
|
9427
9256
|
Sidebar,
|
|
9428
9257
|
SidebarContent,
|
|
9429
9258
|
SidebarFooter,
|
|
9259
|
+
SidebarGroup,
|
|
9260
|
+
SidebarGroupLabel,
|
|
9430
9261
|
SidebarHeader,
|
|
9431
9262
|
SidebarMenu,
|
|
9432
|
-
SidebarMenuButton,
|
|
9433
9263
|
SidebarMenuItem,
|
|
9434
|
-
SidebarMenuSub,
|
|
9435
|
-
SidebarMenuSubButton,
|
|
9436
|
-
SidebarMenuSubItem,
|
|
9437
|
-
SidebarRail,
|
|
9438
|
-
SidebarTrigger,
|
|
9439
9264
|
} from '@cms/components/ui/sidebar'
|
|
9440
9265
|
import { cms } from '@cms/data/cms'
|
|
9441
9266
|
import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
|
|
9442
|
-
import {
|
|
9267
|
+
import { Settings, Users } from 'lucide-react'
|
|
9443
9268
|
import Link from 'next/link'
|
|
9269
|
+
import { Fragment } from 'react'
|
|
9270
|
+
import { getSetting } from '@/cms/lib/actions/settings'
|
|
9271
|
+
import { CmsNavLink } from './cms-nav-link'
|
|
9272
|
+
import { CmsSearch } from './cms-search'
|
|
9444
9273
|
|
|
9445
|
-
function
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
<CollapsibleContent>
|
|
9458
|
-
<SidebarMenuSub>
|
|
9459
|
-
{item.children.map((child) => (
|
|
9460
|
-
<SidebarMenuSubItem key={child.href}>
|
|
9461
|
-
<SidebarMenuSubButton asChild>
|
|
9462
|
-
<Link href={child.href}>
|
|
9463
|
-
{child.icon && <child.icon className="size-3.5!" />}
|
|
9464
|
-
<span>{child.label}</span>
|
|
9465
|
-
</Link>
|
|
9466
|
-
</SidebarMenuSubButton>
|
|
9467
|
-
</SidebarMenuSubItem>
|
|
9468
|
-
))}
|
|
9469
|
-
</SidebarMenuSub>
|
|
9470
|
-
</CollapsibleContent>
|
|
9471
|
-
</SidebarMenuItem>
|
|
9472
|
-
</Collapsible>
|
|
9473
|
-
)
|
|
9274
|
+
function groupNavItems(items: CmsNavigationItem[]) {
|
|
9275
|
+
const groups: { label: string | null; items: CmsNavigationItem[] }[] = []
|
|
9276
|
+
const groupMap = new Map<string | null, CmsNavigationItem[]>()
|
|
9277
|
+
|
|
9278
|
+
for (const item of items) {
|
|
9279
|
+
const key = item.group ?? null
|
|
9280
|
+
if (!groupMap.has(key)) {
|
|
9281
|
+
const arr: CmsNavigationItem[] = []
|
|
9282
|
+
groupMap.set(key, arr)
|
|
9283
|
+
groups.push({ label: key, items: arr })
|
|
9284
|
+
}
|
|
9285
|
+
groupMap.get(key)!.push(item)
|
|
9474
9286
|
}
|
|
9475
9287
|
|
|
9476
|
-
return
|
|
9477
|
-
<SidebarMenuItem className="px-2">
|
|
9478
|
-
<SidebarMenuButton asChild>
|
|
9479
|
-
<Link href={item.href}>
|
|
9480
|
-
{item.icon && <item.icon className="size-3.5!" />}
|
|
9481
|
-
<span>{item.label}</span>
|
|
9482
|
-
</Link>
|
|
9483
|
-
</SidebarMenuButton>
|
|
9484
|
-
</SidebarMenuItem>
|
|
9485
|
-
)
|
|
9288
|
+
return groups
|
|
9486
9289
|
}
|
|
9487
9290
|
|
|
9488
9291
|
export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
9489
9292
|
const session = await getSession()
|
|
9490
9293
|
const settings = await getSetting()
|
|
9491
9294
|
const user = session?.user ?? null
|
|
9295
|
+
const groups = groupNavItems(cmsNavigation)
|
|
9492
9296
|
|
|
9493
9297
|
return (
|
|
9494
9298
|
<Sidebar collapsible="icon" {...props}>
|
|
9495
9299
|
<SidebarHeader className="border-b border-border h-14 items-center flex w-full">
|
|
9496
9300
|
<div className="flex items-center gap-2 w-full relative h-full">
|
|
9497
9301
|
<Link href="/cms" className="flex items-center gap-2 w-full">
|
|
9498
|
-
<Avatar className="size-
|
|
9302
|
+
<Avatar className="size-8">
|
|
9499
9303
|
<AvatarImage src={'/favicon.ico'} />
|
|
9500
|
-
<AvatarFallback className="text-sm font-semibold">
|
|
9304
|
+
<AvatarFallback className="text-sm font-semibold text-foreground">
|
|
9501
9305
|
{settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
|
|
9502
9306
|
</AvatarFallback>
|
|
9503
9307
|
</Avatar>
|
|
9504
|
-
<div className="flex items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
|
|
9505
|
-
<span className="text-sm font-
|
|
9308
|
+
<div className="flex text-foreground items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
|
|
9309
|
+
<span className="text-sm font-medium line-clamp-1">
|
|
9310
|
+
{settings?.siteName ?? cms.name}
|
|
9311
|
+
</span>
|
|
9506
9312
|
</div>
|
|
9507
9313
|
</Link>
|
|
9508
|
-
<SidebarTrigger className="hidden md:flex" />
|
|
9509
9314
|
</div>
|
|
9510
9315
|
</SidebarHeader>
|
|
9511
|
-
<SidebarContent
|
|
9512
|
-
<
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
<
|
|
9521
|
-
|
|
9316
|
+
<SidebarContent>
|
|
9317
|
+
<SidebarGroup className="pb-1">
|
|
9318
|
+
<CmsSearch />
|
|
9319
|
+
</SidebarGroup>
|
|
9320
|
+
|
|
9321
|
+
{groups.map((group) => (
|
|
9322
|
+
<Fragment key={group.label ?? '_ungrouped'}>
|
|
9323
|
+
<SidebarGroup>
|
|
9324
|
+
{group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
|
|
9325
|
+
<SidebarMenu>
|
|
9326
|
+
{group.items.map((item) => (
|
|
9327
|
+
<SidebarMenuItem key={item.href}>
|
|
9328
|
+
<CmsNavLink href={item.href}>
|
|
9329
|
+
{item.icon && (
|
|
9330
|
+
<item.icon className="text-muted-foreground" absoluteStrokeWidth />
|
|
9331
|
+
)}
|
|
9332
|
+
<span>{item.label}</span>
|
|
9333
|
+
</CmsNavLink>
|
|
9334
|
+
</SidebarMenuItem>
|
|
9335
|
+
))}
|
|
9336
|
+
</SidebarMenu>
|
|
9337
|
+
</SidebarGroup>
|
|
9338
|
+
</Fragment>
|
|
9339
|
+
))}
|
|
9340
|
+
|
|
9341
|
+
<SidebarGroup className="mt-auto">
|
|
9342
|
+
<SidebarMenu>
|
|
9343
|
+
<SidebarMenuItem>
|
|
9344
|
+
<CmsNavLink href="/cms/users">
|
|
9345
|
+
<Users />
|
|
9522
9346
|
<span>Users</span>
|
|
9523
|
-
</
|
|
9524
|
-
</
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
<Link href="/cms/settings">
|
|
9529
|
-
<Settings className="size-3.5!" />
|
|
9347
|
+
</CmsNavLink>
|
|
9348
|
+
</SidebarMenuItem>
|
|
9349
|
+
<SidebarMenuItem>
|
|
9350
|
+
<CmsNavLink href="/cms/settings">
|
|
9351
|
+
<Settings />
|
|
9530
9352
|
<span>Settings</span>
|
|
9531
|
-
</
|
|
9532
|
-
</
|
|
9533
|
-
</
|
|
9534
|
-
</
|
|
9353
|
+
</CmsNavLink>
|
|
9354
|
+
</SidebarMenuItem>
|
|
9355
|
+
</SidebarMenu>
|
|
9356
|
+
</SidebarGroup>
|
|
9535
9357
|
</SidebarContent>
|
|
9536
9358
|
<SidebarFooter>
|
|
9537
9359
|
{user && (
|
|
@@ -9543,7 +9365,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
9543
9365
|
</div>
|
|
9544
9366
|
)}
|
|
9545
9367
|
</SidebarFooter>
|
|
9546
|
-
<SidebarRail />
|
|
9547
9368
|
</Sidebar>
|
|
9548
9369
|
)
|
|
9549
9370
|
}
|
|
@@ -9563,7 +9384,7 @@ import {
|
|
|
9563
9384
|
AlertDialogFooter,
|
|
9564
9385
|
AlertDialogHeader,
|
|
9565
9386
|
AlertDialogTitle,
|
|
9566
|
-
AlertDialogTrigger
|
|
9387
|
+
AlertDialogTrigger,
|
|
9567
9388
|
} from '@cms/components/ui/alert-dialog'
|
|
9568
9389
|
import { Button } from '@cms/components/ui/button'
|
|
9569
9390
|
import { Trash2 } from 'lucide-react'
|
|
@@ -9585,7 +9406,7 @@ export function DeleteDialog({
|
|
|
9585
9406
|
isPending = false,
|
|
9586
9407
|
title = 'Are you sure?',
|
|
9587
9408
|
description = 'This action cannot be undone. This will permanently delete this item.',
|
|
9588
|
-
trigger
|
|
9409
|
+
trigger,
|
|
9589
9410
|
}: DeleteDialogProps) {
|
|
9590
9411
|
return (
|
|
9591
9412
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -9634,18 +9455,24 @@ export function DeleteButton({ onClick, label = 'item', count }: DeleteButtonPro
|
|
|
9634
9455
|
function pageHeaderTemplate() {
|
|
9635
9456
|
return `interface PageHeaderProps {
|
|
9636
9457
|
title: string
|
|
9637
|
-
description: string
|
|
9638
9458
|
children?: React.ReactNode
|
|
9459
|
+
search?: React.ReactNode
|
|
9460
|
+
actions?: React.ReactNode
|
|
9461
|
+
back?: React.ReactNode
|
|
9639
9462
|
}
|
|
9640
9463
|
|
|
9641
|
-
export function PageHeader({ title,
|
|
9464
|
+
export function PageHeader({ title, children, search, actions, back }: PageHeaderProps) {
|
|
9642
9465
|
return (
|
|
9643
|
-
<div className="
|
|
9644
|
-
<div className="flex
|
|
9645
|
-
|
|
9646
|
-
<
|
|
9466
|
+
<div className="grid grid-cols-3 items-center justify-between w-full h-14 px-4 border-b border-border">
|
|
9467
|
+
<div className="flex items-center justify-start gap-2 w-full">{back && back}</div>
|
|
9468
|
+
<div className="flex items-center justify-center gap-2">
|
|
9469
|
+
<h2 className="text-sm font-medium tracking-tight">{title}</h2>
|
|
9470
|
+
</div>
|
|
9471
|
+
<div className="flex items-center justify-end gap-2">
|
|
9472
|
+
{children && children}
|
|
9473
|
+
{search && search}
|
|
9474
|
+
{actions && actions}
|
|
9647
9475
|
</div>
|
|
9648
|
-
{children && <div className="flex items-center gap-2">{children}</div>}
|
|
9649
9476
|
</div>
|
|
9650
9477
|
)
|
|
9651
9478
|
}
|
|
@@ -9664,7 +9491,7 @@ const statusStyles: Record<StatusVariant, string> = {
|
|
|
9664
9491
|
success: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
|
|
9665
9492
|
warning: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
|
|
9666
9493
|
error: 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300',
|
|
9667
|
-
info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300'
|
|
9494
|
+
info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
|
|
9668
9495
|
}
|
|
9669
9496
|
|
|
9670
9497
|
interface StatusBadgeProps extends Omit<BadgeProps, 'variant'> {
|
|
@@ -9682,7 +9509,11 @@ export function StatusBadge({ status, className, ...props }: StatusBadgeProps) {
|
|
|
9682
9509
|
}
|
|
9683
9510
|
|
|
9684
9511
|
/** Map boolean values to status badges */
|
|
9685
|
-
export function BooleanBadge({
|
|
9512
|
+
export function BooleanBadge({
|
|
9513
|
+
value,
|
|
9514
|
+
trueLabel = 'Yes',
|
|
9515
|
+
falseLabel = 'No',
|
|
9516
|
+
}: {
|
|
9686
9517
|
value: boolean
|
|
9687
9518
|
trueLabel?: string
|
|
9688
9519
|
falseLabel?: string
|
|
@@ -9706,22 +9537,44 @@ function cmsDataTemplate(projectName) {
|
|
|
9706
9537
|
|
|
9707
9538
|
// src/init/templates/data/navigation.ts
|
|
9708
9539
|
function navigationDataTemplate() {
|
|
9709
|
-
return `import {
|
|
9710
|
-
import
|
|
9540
|
+
return `import type { LucideIcon } from 'lucide-react'
|
|
9541
|
+
import { ChartSpline, FileText, House, ImagePlay, Tag } from 'lucide-react'
|
|
9711
9542
|
|
|
9712
9543
|
export interface CmsNavigationItem {
|
|
9713
9544
|
label: string
|
|
9714
9545
|
href: string
|
|
9715
9546
|
icon?: LucideIcon
|
|
9716
|
-
|
|
9547
|
+
group?: string
|
|
9717
9548
|
}
|
|
9718
9549
|
|
|
9719
9550
|
export const cmsNavigation: CmsNavigationItem[] = [
|
|
9720
9551
|
{
|
|
9721
|
-
label: '
|
|
9552
|
+
label: 'Overview',
|
|
9722
9553
|
href: '/cms',
|
|
9723
|
-
icon: House
|
|
9724
|
-
}
|
|
9554
|
+
icon: House,
|
|
9555
|
+
},
|
|
9556
|
+
{
|
|
9557
|
+
label: 'Analytics',
|
|
9558
|
+
href: '/cms/analytics',
|
|
9559
|
+
icon: ChartSpline,
|
|
9560
|
+
},
|
|
9561
|
+
{
|
|
9562
|
+
label: 'Media',
|
|
9563
|
+
href: '/cms/media',
|
|
9564
|
+
icon: ImagePlay,
|
|
9565
|
+
},
|
|
9566
|
+
{
|
|
9567
|
+
label: 'Categories',
|
|
9568
|
+
href: '/cms/categories',
|
|
9569
|
+
icon: Tag,
|
|
9570
|
+
group: 'Blog',
|
|
9571
|
+
},
|
|
9572
|
+
{
|
|
9573
|
+
label: 'Posts',
|
|
9574
|
+
href: '/cms/posts',
|
|
9575
|
+
icon: FileText,
|
|
9576
|
+
group: 'Blog',
|
|
9577
|
+
},
|
|
9725
9578
|
]
|
|
9726
9579
|
`;
|
|
9727
9580
|
}
|
|
@@ -9795,14 +9648,10 @@ export function CmsThemeProvider({ children }: { children: React.ReactNode }) {
|
|
|
9795
9648
|
|
|
9796
9649
|
const value = React.useMemo(
|
|
9797
9650
|
() => ({ theme, setTheme, resolvedTheme: resolved }),
|
|
9798
|
-
[theme, setTheme, resolved]
|
|
9651
|
+
[theme, setTheme, resolved],
|
|
9799
9652
|
)
|
|
9800
9653
|
|
|
9801
|
-
return
|
|
9802
|
-
<CmsThemeContext.Provider value={value}>
|
|
9803
|
-
{children}
|
|
9804
|
-
</CmsThemeContext.Provider>
|
|
9805
|
-
)
|
|
9654
|
+
return <CmsThemeContext.Provider value={value}>{children}</CmsThemeContext.Provider>
|
|
9806
9655
|
}
|
|
9807
9656
|
|
|
9808
9657
|
export function useTheme(): ThemeContext {
|
|
@@ -9842,11 +9691,11 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9842
9691
|
if (result.success && result.files) {
|
|
9843
9692
|
const images: EditorImageUploadResult[] = result.files.map((f) => ({
|
|
9844
9693
|
url: f.url,
|
|
9845
|
-
filename: f.filename
|
|
9694
|
+
filename: f.filename,
|
|
9846
9695
|
}))
|
|
9847
9696
|
onImagesUploadedRef.current(images)
|
|
9848
9697
|
}
|
|
9849
|
-
}
|
|
9698
|
+
},
|
|
9850
9699
|
})
|
|
9851
9700
|
|
|
9852
9701
|
const isUploading = mutation.isPending
|
|
@@ -9857,21 +9706,19 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9857
9706
|
if (imageFiles.length === 0) return
|
|
9858
9707
|
upload(imageFiles, 'images')
|
|
9859
9708
|
},
|
|
9860
|
-
[upload]
|
|
9709
|
+
[upload],
|
|
9861
9710
|
)
|
|
9862
9711
|
|
|
9863
9712
|
const handleDrop = React.useCallback(
|
|
9864
9713
|
(e: React.DragEvent) => {
|
|
9865
9714
|
e.preventDefault()
|
|
9866
9715
|
e.stopPropagation()
|
|
9867
|
-
const files = Array.from(e.dataTransfer.files).filter((f) =>
|
|
9868
|
-
f.type.startsWith('image/')
|
|
9869
|
-
)
|
|
9716
|
+
const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'))
|
|
9870
9717
|
if (files.length > 0) {
|
|
9871
9718
|
uploadImages(files)
|
|
9872
9719
|
}
|
|
9873
9720
|
},
|
|
9874
|
-
[uploadImages]
|
|
9721
|
+
[uploadImages],
|
|
9875
9722
|
)
|
|
9876
9723
|
|
|
9877
9724
|
const openFilePicker = React.useCallback(() => {
|
|
@@ -9886,7 +9733,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9886
9733
|
}
|
|
9887
9734
|
e.target.value = ''
|
|
9888
9735
|
},
|
|
9889
|
-
[uploadImages]
|
|
9736
|
+
[uploadImages],
|
|
9890
9737
|
)
|
|
9891
9738
|
|
|
9892
9739
|
return {
|
|
@@ -9896,7 +9743,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9896
9743
|
handleDrop,
|
|
9897
9744
|
openFilePicker,
|
|
9898
9745
|
fileInputRef,
|
|
9899
|
-
handleFileInputChange
|
|
9746
|
+
handleFileInputChange,
|
|
9900
9747
|
}
|
|
9901
9748
|
}
|
|
9902
9749
|
`;
|
|
@@ -9920,7 +9767,7 @@ export function useLocalStorage<T>(key: string) {
|
|
|
9920
9767
|
// Silent failure for localStorage access errors
|
|
9921
9768
|
}
|
|
9922
9769
|
},
|
|
9923
|
-
[prefixedKey]
|
|
9770
|
+
[prefixedKey],
|
|
9924
9771
|
)
|
|
9925
9772
|
|
|
9926
9773
|
const getItem = React.useCallback((): T | null => {
|
|
@@ -9953,18 +9800,36 @@ export function useLocalStorage<T>(key: string) {
|
|
|
9953
9800
|
`;
|
|
9954
9801
|
}
|
|
9955
9802
|
|
|
9803
|
+
// src/init/templates/hooks/use-mobile.ts
|
|
9804
|
+
function useMobileHookTemplate() {
|
|
9805
|
+
return `import * as React from 'react'
|
|
9806
|
+
|
|
9807
|
+
const MOBILE_BREAKPOINT = 768
|
|
9808
|
+
|
|
9809
|
+
export function useIsMobile() {
|
|
9810
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
9811
|
+
|
|
9812
|
+
React.useEffect(() => {
|
|
9813
|
+
const mql = window.matchMedia(\`(max-width: \${MOBILE_BREAKPOINT - 1}px)\`)
|
|
9814
|
+
const onChange = () => {
|
|
9815
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
9816
|
+
}
|
|
9817
|
+
mql.addEventListener('change', onChange)
|
|
9818
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
9819
|
+
return () => mql.removeEventListener('change', onChange)
|
|
9820
|
+
}, [])
|
|
9821
|
+
|
|
9822
|
+
return !!isMobile
|
|
9823
|
+
}
|
|
9824
|
+
`;
|
|
9825
|
+
}
|
|
9826
|
+
|
|
9956
9827
|
// src/init/templates/hooks/use-upload.ts
|
|
9957
9828
|
function useUploadHookTemplate() {
|
|
9958
9829
|
return `'use client'
|
|
9959
9830
|
|
|
9960
|
-
import type {
|
|
9961
|
-
|
|
9962
|
-
UploadProgress
|
|
9963
|
-
} from '@cms/types'
|
|
9964
|
-
import {
|
|
9965
|
-
type FileValidationConfig,
|
|
9966
|
-
validateFiles
|
|
9967
|
-
} from '@cms/utils/validation'
|
|
9831
|
+
import type { UploadFileResult, UploadProgress } from '@cms/types'
|
|
9832
|
+
import { type FileValidationConfig, validateFiles } from '@cms/utils/validation'
|
|
9968
9833
|
import { type UseMutationResult, useMutation } from '@tanstack/react-query'
|
|
9969
9834
|
import * as React from 'react'
|
|
9970
9835
|
|
|
@@ -10012,7 +9877,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10012
9877
|
onProgress,
|
|
10013
9878
|
onSuccess,
|
|
10014
9879
|
onError,
|
|
10015
|
-
prefix: defaultPrefix
|
|
9880
|
+
prefix: defaultPrefix,
|
|
10016
9881
|
} = options
|
|
10017
9882
|
|
|
10018
9883
|
const validationConfig = React.useMemo<FileValidationConfig>(() => {
|
|
@@ -10022,7 +9887,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10022
9887
|
const parsedTypes = parseAcceptTypes(accept)
|
|
10023
9888
|
if (config.allowedTypes && config.allowedTypes.length > 0) {
|
|
10024
9889
|
config.allowedTypes = [...config.allowedTypes, ...parsedTypes].filter(
|
|
10025
|
-
(v, i, a) => a.indexOf(v) === i
|
|
9890
|
+
(v, i, a) => a.indexOf(v) === i,
|
|
10026
9891
|
)
|
|
10027
9892
|
} else {
|
|
10028
9893
|
config.allowedTypes = parsedTypes
|
|
@@ -10045,7 +9910,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10045
9910
|
filename: file.name,
|
|
10046
9911
|
progress: 0,
|
|
10047
9912
|
loaded: 0,
|
|
10048
|
-
total: file.size
|
|
9913
|
+
total: file.size,
|
|
10049
9914
|
}))
|
|
10050
9915
|
setProgress(initialProgress)
|
|
10051
9916
|
onProgress?.(initialProgress)
|
|
@@ -10067,9 +9932,9 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10067
9932
|
return {
|
|
10068
9933
|
...p,
|
|
10069
9934
|
progress: newProgress,
|
|
10070
|
-
loaded: Math.floor((p.total * newProgress) / 100)
|
|
9935
|
+
loaded: Math.floor((p.total * newProgress) / 100),
|
|
10071
9936
|
}
|
|
10072
|
-
})
|
|
9937
|
+
}),
|
|
10073
9938
|
)
|
|
10074
9939
|
}, 200)
|
|
10075
9940
|
|
|
@@ -10083,7 +9948,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10083
9948
|
|
|
10084
9949
|
const response = await fetch('/api/cms/upload', {
|
|
10085
9950
|
method: 'POST',
|
|
10086
|
-
body: formData
|
|
9951
|
+
body: formData,
|
|
10087
9952
|
})
|
|
10088
9953
|
|
|
10089
9954
|
const result = (await response.json()) as UploadFileResult
|
|
@@ -10093,7 +9958,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10093
9958
|
filename: file.name,
|
|
10094
9959
|
progress: 100,
|
|
10095
9960
|
loaded: file.size,
|
|
10096
|
-
total: file.size
|
|
9961
|
+
total: file.size,
|
|
10097
9962
|
}))
|
|
10098
9963
|
setProgress(completeProgress)
|
|
10099
9964
|
onProgress?.(completeProgress)
|
|
@@ -10115,14 +9980,14 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10115
9980
|
onError: (error) => {
|
|
10116
9981
|
onError?.(error)
|
|
10117
9982
|
setProgress([])
|
|
10118
|
-
}
|
|
9983
|
+
},
|
|
10119
9984
|
})
|
|
10120
9985
|
|
|
10121
9986
|
const upload = React.useCallback(
|
|
10122
9987
|
(files: File[], prefix?: string) => {
|
|
10123
9988
|
mutation.mutate({ files, prefix })
|
|
10124
9989
|
},
|
|
10125
|
-
[mutation]
|
|
9990
|
+
[mutation],
|
|
10126
9991
|
)
|
|
10127
9992
|
|
|
10128
9993
|
const validate = React.useCallback(
|
|
@@ -10130,10 +9995,10 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10130
9995
|
const result = validateFiles(files, validationConfig)
|
|
10131
9996
|
return {
|
|
10132
9997
|
valid: result.valid,
|
|
10133
|
-
errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`)
|
|
9998
|
+
errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`),
|
|
10134
9999
|
}
|
|
10135
10000
|
},
|
|
10136
|
-
[validationConfig]
|
|
10001
|
+
[validationConfig],
|
|
10137
10002
|
)
|
|
10138
10003
|
|
|
10139
10004
|
return { mutation, progress, upload, validate }
|
|
@@ -10153,7 +10018,7 @@ export function useUsers() {
|
|
|
10153
10018
|
return useQuery<UsersResponse>({
|
|
10154
10019
|
queryKey: ['users'],
|
|
10155
10020
|
queryFn: () => getUsers(),
|
|
10156
|
-
staleTime: 0
|
|
10021
|
+
staleTime: 0,
|
|
10157
10022
|
})
|
|
10158
10023
|
}
|
|
10159
10024
|
`;
|
|
@@ -10163,9 +10028,9 @@ export function useUsers() {
|
|
|
10163
10028
|
function formSettingsActionTemplate() {
|
|
10164
10029
|
return `'use server'
|
|
10165
10030
|
|
|
10166
|
-
import { eq } from 'drizzle-orm'
|
|
10167
10031
|
import db from '@cms/db'
|
|
10168
10032
|
import { formSettings } from '@cms/db/schema'
|
|
10033
|
+
import { eq } from 'drizzle-orm'
|
|
10169
10034
|
|
|
10170
10035
|
export interface FormSettingsData {
|
|
10171
10036
|
id: number
|
|
@@ -10189,9 +10054,7 @@ export interface FormSettingsResult {
|
|
|
10189
10054
|
settings?: FormSettingsData
|
|
10190
10055
|
}
|
|
10191
10056
|
|
|
10192
|
-
export async function getFormSettings(
|
|
10193
|
-
formName: string
|
|
10194
|
-
): Promise<FormSettingsData | null> {
|
|
10057
|
+
export async function getFormSettings(formName: string): Promise<FormSettingsData | null> {
|
|
10195
10058
|
try {
|
|
10196
10059
|
const [settings] = await db
|
|
10197
10060
|
.select()
|
|
@@ -10207,7 +10070,7 @@ export async function getFormSettings(
|
|
|
10207
10070
|
|
|
10208
10071
|
export async function upsertFormSettings(
|
|
10209
10072
|
formName: string,
|
|
10210
|
-
data: UpsertFormSettingsInput
|
|
10073
|
+
data: UpsertFormSettingsInput,
|
|
10211
10074
|
): Promise<FormSettingsResult> {
|
|
10212
10075
|
try {
|
|
10213
10076
|
const existing = await getFormSettings(formName)
|
|
@@ -10238,8 +10101,7 @@ export async function upsertFormSettings(
|
|
|
10238
10101
|
console.error(\`Error upserting form settings for \${formName}:\`, error)
|
|
10239
10102
|
return {
|
|
10240
10103
|
success: false,
|
|
10241
|
-
error:
|
|
10242
|
-
error instanceof Error ? error.message : 'Failed to save form settings',
|
|
10104
|
+
error: error instanceof Error ? error.message : 'Failed to save form settings',
|
|
10243
10105
|
}
|
|
10244
10106
|
}
|
|
10245
10107
|
}
|
|
@@ -10255,7 +10117,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
|
|
|
10255
10117
|
}
|
|
10256
10118
|
|
|
10257
10119
|
export async function testFormWebhook(
|
|
10258
|
-
formName: string
|
|
10120
|
+
formName: string,
|
|
10259
10121
|
): Promise<{ success: boolean; error?: string }> {
|
|
10260
10122
|
try {
|
|
10261
10123
|
const settings = await getFormSettings(formName)
|
|
@@ -10286,8 +10148,7 @@ export async function testFormWebhook(
|
|
|
10286
10148
|
} catch (error) {
|
|
10287
10149
|
return {
|
|
10288
10150
|
success: false,
|
|
10289
|
-
error:
|
|
10290
|
-
error instanceof Error ? error.message : 'Failed to send test webhook',
|
|
10151
|
+
error: error instanceof Error ? error.message : 'Failed to send test webhook',
|
|
10291
10152
|
}
|
|
10292
10153
|
}
|
|
10293
10154
|
}
|
|
@@ -10456,10 +10317,10 @@ export async function uploadImageFromUrl(
|
|
|
10456
10317
|
function usersActionTemplate() {
|
|
10457
10318
|
return `'use server'
|
|
10458
10319
|
|
|
10320
|
+
import { auth } from '@cms/auth'
|
|
10459
10321
|
import db from '@cms/db'
|
|
10460
|
-
import { user
|
|
10322
|
+
import { user } from '@cms/db/schema'
|
|
10461
10323
|
import { eq } from 'drizzle-orm'
|
|
10462
|
-
import { auth } from '@cms/auth'
|
|
10463
10324
|
|
|
10464
10325
|
export interface UserData {
|
|
10465
10326
|
id: string
|
|
@@ -10572,15 +10433,9 @@ export async function getUsers(): Promise<UsersResponse> {
|
|
|
10572
10433
|
/**
|
|
10573
10434
|
* Update a user's role
|
|
10574
10435
|
*/
|
|
10575
|
-
export async function updateUserRole(
|
|
10576
|
-
userId: string,
|
|
10577
|
-
role: string,
|
|
10578
|
-
): Promise<UpdateUserRoleResult> {
|
|
10436
|
+
export async function updateUserRole(userId: string, role: string): Promise<UpdateUserRoleResult> {
|
|
10579
10437
|
try {
|
|
10580
|
-
await db
|
|
10581
|
-
.update(user)
|
|
10582
|
-
.set({ role, updatedAt: new Date() })
|
|
10583
|
-
.where(eq(user.id, userId))
|
|
10438
|
+
await db.update(user).set({ role, updatedAt: new Date() }).where(eq(user.id, userId))
|
|
10584
10439
|
|
|
10585
10440
|
return { success: true }
|
|
10586
10441
|
} catch (error) {
|
|
@@ -10730,10 +10585,21 @@ function trimMathBlock(content: string): string {
|
|
|
10730
10585
|
const shiki = createHighlighterCoreSync({
|
|
10731
10586
|
themes: [githubDark, githubLight],
|
|
10732
10587
|
langs: [
|
|
10733
|
-
javascript,
|
|
10734
|
-
|
|
10588
|
+
javascript,
|
|
10589
|
+
typescript,
|
|
10590
|
+
jsx,
|
|
10591
|
+
tsx,
|
|
10592
|
+
python,
|
|
10593
|
+
rust,
|
|
10594
|
+
go,
|
|
10595
|
+
json,
|
|
10596
|
+
yaml,
|
|
10597
|
+
css,
|
|
10598
|
+
sql,
|
|
10599
|
+
shellscript,
|
|
10600
|
+
markdown,
|
|
10735
10601
|
],
|
|
10736
|
-
engine: createJavaScriptRegexEngine()
|
|
10602
|
+
engine: createJavaScriptRegexEngine(),
|
|
10737
10603
|
})
|
|
10738
10604
|
|
|
10739
10605
|
const loadedLangs = shiki.getLoadedLanguages()
|
|
@@ -10775,11 +10641,11 @@ const md = MarkdownIt({
|
|
|
10775
10641
|
lang: language,
|
|
10776
10642
|
themes: { light: 'github-light', dark: 'github-dark' },
|
|
10777
10643
|
defaultColor: false,
|
|
10778
|
-
transformers: [transformerNotationHighlight(), transformerNotationDiff()]
|
|
10644
|
+
transformers: [transformerNotationHighlight(), transformerNotationDiff()],
|
|
10779
10645
|
})
|
|
10780
10646
|
const escapedCode = code.replace(/</g, '<').replace(/>/g, '>')
|
|
10781
10647
|
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>\`
|
|
10782
|
-
}
|
|
10648
|
+
},
|
|
10783
10649
|
})
|
|
10784
10650
|
.use(headingAnchorPlugin)
|
|
10785
10651
|
.use(dollarmath, {
|
|
@@ -10792,7 +10658,7 @@ const md = MarkdownIt({
|
|
|
10792
10658
|
return renderToString(content, {
|
|
10793
10659
|
displayMode,
|
|
10794
10660
|
throwOnError: false,
|
|
10795
|
-
strict: 'ignore'
|
|
10661
|
+
strict: 'ignore',
|
|
10796
10662
|
})
|
|
10797
10663
|
},
|
|
10798
10664
|
labelNormalizer(label: string) {
|
|
@@ -10800,7 +10666,7 @@ const md = MarkdownIt({
|
|
|
10800
10666
|
},
|
|
10801
10667
|
labelRenderer(label: string) {
|
|
10802
10668
|
return \`<a href="#\${label}" class="mathlabel" title="Permalink to this equation">\xB6</a>\`
|
|
10803
|
-
}
|
|
10669
|
+
},
|
|
10804
10670
|
})
|
|
10805
10671
|
|
|
10806
10672
|
export function renderMarkdownSync(src: string): string {
|
|
@@ -10895,7 +10761,7 @@ export interface AuthSession {
|
|
|
10895
10761
|
export enum UserRole {
|
|
10896
10762
|
ADMIN = 'admin',
|
|
10897
10763
|
EDITOR = 'editor',
|
|
10898
|
-
MEMBER = 'member'
|
|
10764
|
+
MEMBER = 'member',
|
|
10899
10765
|
}
|
|
10900
10766
|
|
|
10901
10767
|
export interface UserWithRole extends AuthUser {
|
|
@@ -10904,17 +10770,11 @@ export interface UserWithRole extends AuthUser {
|
|
|
10904
10770
|
|
|
10905
10771
|
/** Type guard to check if a value is a valid UserRole */
|
|
10906
10772
|
export function isUserRole(value: unknown): value is UserRole {
|
|
10907
|
-
return (
|
|
10908
|
-
typeof value === 'string' &&
|
|
10909
|
-
Object.values(UserRole).includes(value as UserRole)
|
|
10910
|
-
)
|
|
10773
|
+
return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
|
|
10911
10774
|
}
|
|
10912
10775
|
|
|
10913
10776
|
/** Check if user has one of the allowed roles */
|
|
10914
|
-
export function hasRequiredRole(
|
|
10915
|
-
userRole: UserRole,
|
|
10916
|
-
allowedRoles: UserRole[]
|
|
10917
|
-
): boolean {
|
|
10777
|
+
export function hasRequiredRole(userRole: UserRole, allowedRoles: UserRole[]): boolean {
|
|
10918
10778
|
return allowedRoles.includes(userRole)
|
|
10919
10779
|
}
|
|
10920
10780
|
|
|
@@ -11100,7 +10960,7 @@ export function createMetadata({
|
|
|
11100
10960
|
description,
|
|
11101
10961
|
path,
|
|
11102
10962
|
ogImage,
|
|
11103
|
-
noIndex = false
|
|
10963
|
+
noIndex = false,
|
|
11104
10964
|
}: CreateMetadataOptions): Metadata {
|
|
11105
10965
|
const metadata: Metadata = {
|
|
11106
10966
|
title,
|
|
@@ -11109,14 +10969,14 @@ export function createMetadata({
|
|
|
11109
10969
|
title,
|
|
11110
10970
|
description,
|
|
11111
10971
|
type: 'website',
|
|
11112
|
-
...(ogImage && { images: [{ url: ogImage }] })
|
|
10972
|
+
...(ogImage && { images: [{ url: ogImage }] }),
|
|
11113
10973
|
},
|
|
11114
10974
|
twitter: {
|
|
11115
10975
|
card: ogImage ? 'summary_large_image' : 'summary',
|
|
11116
10976
|
title,
|
|
11117
10977
|
description,
|
|
11118
|
-
...(ogImage && { images: [ogImage] })
|
|
11119
|
-
}
|
|
10978
|
+
...(ogImage && { images: [ogImage] }),
|
|
10979
|
+
},
|
|
11120
10980
|
}
|
|
11121
10981
|
|
|
11122
10982
|
if (path) {
|
|
@@ -11140,7 +11000,7 @@ export function generateArticleSchema({
|
|
|
11140
11000
|
imageUrl,
|
|
11141
11001
|
datePublished,
|
|
11142
11002
|
dateModified,
|
|
11143
|
-
authorName
|
|
11003
|
+
authorName,
|
|
11144
11004
|
}: {
|
|
11145
11005
|
title: string
|
|
11146
11006
|
description: string
|
|
@@ -11160,8 +11020,8 @@ export function generateArticleSchema({
|
|
|
11160
11020
|
datePublished,
|
|
11161
11021
|
...(dateModified && { dateModified }),
|
|
11162
11022
|
...(authorName && {
|
|
11163
|
-
author: { '@type': 'Person', name: authorName }
|
|
11164
|
-
})
|
|
11023
|
+
author: { '@type': 'Person', name: authorName },
|
|
11024
|
+
}),
|
|
11165
11025
|
}
|
|
11166
11026
|
}
|
|
11167
11027
|
|
|
@@ -11214,20 +11074,16 @@ function isFileTypeAllowed(file: File, allowedTypes: string[]): boolean {
|
|
|
11214
11074
|
*/
|
|
11215
11075
|
export function validateFiles(
|
|
11216
11076
|
files: File[],
|
|
11217
|
-
config: FileValidationConfig = {}
|
|
11077
|
+
config: FileValidationConfig = {},
|
|
11218
11078
|
): FileValidationResult {
|
|
11219
|
-
const {
|
|
11220
|
-
maxSizeInBytes = DEFAULT_MAX_SIZE,
|
|
11221
|
-
allowedTypes,
|
|
11222
|
-
maxFiles = DEFAULT_MAX_FILES
|
|
11223
|
-
} = config
|
|
11079
|
+
const { maxSizeInBytes = DEFAULT_MAX_SIZE, allowedTypes, maxFiles = DEFAULT_MAX_FILES } = config
|
|
11224
11080
|
|
|
11225
11081
|
const errors: FileValidationError[] = []
|
|
11226
11082
|
|
|
11227
11083
|
if (files.length > maxFiles) {
|
|
11228
11084
|
errors.push({
|
|
11229
11085
|
filename: '',
|
|
11230
|
-
error: \`Too many files. Maximum is \${maxFiles}
|
|
11086
|
+
error: \`Too many files. Maximum is \${maxFiles}.\`,
|
|
11231
11087
|
})
|
|
11232
11088
|
}
|
|
11233
11089
|
|
|
@@ -11236,14 +11092,14 @@ export function validateFiles(
|
|
|
11236
11092
|
const maxMB = Math.round(maxSizeInBytes / (1024 * 1024))
|
|
11237
11093
|
errors.push({
|
|
11238
11094
|
filename: file.name,
|
|
11239
|
-
error: \`File exceeds maximum size of \${maxMB}MB
|
|
11095
|
+
error: \`File exceeds maximum size of \${maxMB}MB.\`,
|
|
11240
11096
|
})
|
|
11241
11097
|
}
|
|
11242
11098
|
|
|
11243
11099
|
if (allowedTypes && allowedTypes.length > 0 && !isFileTypeAllowed(file, allowedTypes)) {
|
|
11244
11100
|
errors.push({
|
|
11245
11101
|
filename: file.name,
|
|
11246
|
-
error: \`File type "\${file.type || 'unknown'}" is not allowed
|
|
11102
|
+
error: \`File type "\${file.type || 'unknown'}" is not allowed.\`,
|
|
11247
11103
|
})
|
|
11248
11104
|
}
|
|
11249
11105
|
}
|
|
@@ -11297,17 +11153,15 @@ function webhookUtilTemplate() {
|
|
|
11297
11153
|
*/
|
|
11298
11154
|
export function sendWebhook(
|
|
11299
11155
|
webhookUrl: string | null | undefined,
|
|
11300
|
-
payload: Record<string, unknown
|
|
11156
|
+
payload: Record<string, unknown>,
|
|
11301
11157
|
): void {
|
|
11302
|
-
if (!webhookUrl) return
|
|
11303
|
-
// Fire-and-forget: runs in background, doesn't block
|
|
11158
|
+
if (!webhookUrl) return // Fire-and-forget: runs in background, doesn't block
|
|
11304
11159
|
;(async () => {
|
|
11305
11160
|
try {
|
|
11306
11161
|
const formData = new URLSearchParams()
|
|
11307
11162
|
for (const [key, value] of Object.entries(payload)) {
|
|
11308
11163
|
if (value === null || value === undefined) continue
|
|
11309
|
-
const stringValue =
|
|
11310
|
-
typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
11164
|
+
const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
11311
11165
|
formData.append(key, stringValue)
|
|
11312
11166
|
}
|
|
11313
11167
|
await fetch(webhookUrl, {
|
|
@@ -11335,6 +11189,7 @@ function scaffoldComponents({ cwd, config }) {
|
|
|
11335
11189
|
}
|
|
11336
11190
|
write("cms-globals.css", cmsGlobalsCssTemplate());
|
|
11337
11191
|
write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
|
|
11192
|
+
write("components/layout/cms-nav-link.tsx", cmsNavLinkTemplate());
|
|
11338
11193
|
write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
|
|
11339
11194
|
write("components/layout/cms-header.tsx", cmsHeaderTemplate());
|
|
11340
11195
|
write("components/layout/cms-search.tsx", cmsSearchTemplate());
|
|
@@ -11360,6 +11215,7 @@ function scaffoldComponents({ cwd, config }) {
|
|
|
11360
11215
|
write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
|
|
11361
11216
|
write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
|
|
11362
11217
|
write("hooks/use-users.ts", useUsersHookTemplate());
|
|
11218
|
+
write("hooks/use-mobile.ts", useMobileHookTemplate());
|
|
11363
11219
|
const projectName = detectProjectName(cwd);
|
|
11364
11220
|
write("data/cms.ts", cmsDataTemplate(projectName));
|
|
11365
11221
|
write("data/navigation.ts", navigationDataTemplate());
|
|
@@ -11596,6 +11452,7 @@ var CORE_DEPS = [
|
|
|
11596
11452
|
"nuqs",
|
|
11597
11453
|
"sonner",
|
|
11598
11454
|
// Styling utilities
|
|
11455
|
+
"geist",
|
|
11599
11456
|
"class-variance-authority",
|
|
11600
11457
|
"clsx",
|
|
11601
11458
|
"tailwind-merge",
|
|
@@ -11841,24 +11698,18 @@ import path32 from "path";
|
|
|
11841
11698
|
|
|
11842
11699
|
// src/init/templates/pages/authenticated-layout.ts
|
|
11843
11700
|
function authenticatedLayoutTemplate() {
|
|
11844
|
-
return `import {
|
|
11701
|
+
return `import { requireRole } from '@cms/auth/middleware'
|
|
11845
11702
|
import { CmsSidebar } from '@cms/components/layout/cms-sidebar'
|
|
11846
|
-
import { requireRole } from '@cms/auth/middleware'
|
|
11847
|
-
import { UserRole } from '@cms/types/auth'
|
|
11848
11703
|
import { SidebarInset, SidebarProvider } from '@cms/components/ui/sidebar'
|
|
11704
|
+
import { UserRole } from '@cms/types/auth'
|
|
11849
11705
|
|
|
11850
|
-
export default async function CmsAuthLayout({
|
|
11851
|
-
children
|
|
11852
|
-
}: {
|
|
11853
|
-
children: React.ReactNode
|
|
11854
|
-
}) {
|
|
11706
|
+
export default async function CmsAuthLayout({ children }: { children: React.ReactNode }) {
|
|
11855
11707
|
await requireRole([UserRole.ADMIN, UserRole.EDITOR])
|
|
11856
11708
|
|
|
11857
11709
|
return (
|
|
11858
11710
|
<SidebarProvider>
|
|
11859
11711
|
<CmsSidebar />
|
|
11860
11712
|
<SidebarInset>
|
|
11861
|
-
<CmsHeader />
|
|
11862
11713
|
<main>{children}</main>
|
|
11863
11714
|
</SidebarInset>
|
|
11864
11715
|
</SidebarProvider>
|
|
@@ -11871,11 +11722,17 @@ export default async function CmsAuthLayout({
|
|
|
11871
11722
|
function cmsLayoutTemplate() {
|
|
11872
11723
|
return `import '@cms/cms-globals.css'
|
|
11873
11724
|
import { CmsProviders } from '@cms/components/layout/cms-providers'
|
|
11725
|
+
import { GeistMono } from 'geist/font/mono'
|
|
11726
|
+
import { GeistSans } from 'geist/font/sans'
|
|
11874
11727
|
|
|
11875
11728
|
export default function CmsLayout({ children }: { children: React.ReactNode }) {
|
|
11876
11729
|
return (
|
|
11877
11730
|
<CmsProviders>
|
|
11878
|
-
<div
|
|
11731
|
+
<div
|
|
11732
|
+
className={\`cms-root min-h-screen antialiased \${GeistSans.variable} \${GeistMono.variable}\`}
|
|
11733
|
+
>
|
|
11734
|
+
{children}
|
|
11735
|
+
</div>
|
|
11879
11736
|
</CmsProviders>
|
|
11880
11737
|
)
|
|
11881
11738
|
}
|
|
@@ -11885,8 +11742,8 @@ export default function CmsLayout({ children }: { children: React.ReactNode }) {
|
|
|
11885
11742
|
// src/init/templates/pages/dashboard-page.ts
|
|
11886
11743
|
function dashboardPageTemplate() {
|
|
11887
11744
|
return `import { PageHeader } from '@cms/components/shared/page-header'
|
|
11888
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
|
|
11889
11745
|
import { Badge } from '@cms/components/ui/badge'
|
|
11746
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
|
|
11890
11747
|
import { FileText, Settings, Users } from 'lucide-react'
|
|
11891
11748
|
import Link from 'next/link'
|
|
11892
11749
|
|
|
@@ -11895,32 +11752,27 @@ const quickLinks = [
|
|
|
11895
11752
|
title: 'Users',
|
|
11896
11753
|
description: 'Manage admin users and roles',
|
|
11897
11754
|
href: '/cms/users',
|
|
11898
|
-
icon: Users
|
|
11755
|
+
icon: Users,
|
|
11899
11756
|
},
|
|
11900
11757
|
{
|
|
11901
11758
|
title: 'Settings',
|
|
11902
11759
|
description: 'Configure CMS settings',
|
|
11903
11760
|
href: '/cms/settings',
|
|
11904
|
-
icon: Settings
|
|
11761
|
+
icon: Settings,
|
|
11905
11762
|
},
|
|
11906
11763
|
{
|
|
11907
11764
|
title: 'Generate',
|
|
11908
11765
|
description: 'Add a new resource from a schema',
|
|
11909
11766
|
href: '#',
|
|
11910
11767
|
icon: FileText,
|
|
11911
|
-
hint: 'npx betterstart generate <schema>'
|
|
11912
|
-
}
|
|
11768
|
+
hint: 'npx betterstart generate <schema>',
|
|
11769
|
+
},
|
|
11913
11770
|
]
|
|
11914
11771
|
|
|
11915
11772
|
export default function DashboardPage() {
|
|
11916
11773
|
return (
|
|
11917
11774
|
<div className="flex flex-col">
|
|
11918
|
-
<
|
|
11919
|
-
<PageHeader
|
|
11920
|
-
title="Dashboard"
|
|
11921
|
-
description="Welcome to your CMS admin panel"
|
|
11922
|
-
/>
|
|
11923
|
-
</div>
|
|
11775
|
+
<PageHeader title="Dashboard" />
|
|
11924
11776
|
<div className="p-6 space-y-6">
|
|
11925
11777
|
<div className="grid gap-4 md:grid-cols-3">
|
|
11926
11778
|
{quickLinks.map((link) => (
|
|
@@ -11990,7 +11842,7 @@ import { LoginForm } from './login-form'
|
|
|
11990
11842
|
|
|
11991
11843
|
export const metadata: Metadata = {
|
|
11992
11844
|
title: 'CMS Login',
|
|
11993
|
-
robots: { index: false, follow: false }
|
|
11845
|
+
robots: { index: false, follow: false },
|
|
11994
11846
|
}
|
|
11995
11847
|
|
|
11996
11848
|
export default function LoginPage() {
|
|
@@ -11999,9 +11851,7 @@ export default function LoginPage() {
|
|
|
11999
11851
|
<div className="w-full max-w-sm">
|
|
12000
11852
|
<div className="mb-8 text-center">
|
|
12001
11853
|
<h1 className="text-2xl font-semibold tracking-tight">CMS</h1>
|
|
12002
|
-
<p className="text-muted-foreground text-sm mt-1">
|
|
12003
|
-
Sign in to access the admin panel
|
|
12004
|
-
</p>
|
|
11854
|
+
<p className="text-muted-foreground text-sm mt-1">Sign in to access the admin panel</p>
|
|
12005
11855
|
</div>
|
|
12006
11856
|
<LoginForm />
|
|
12007
11857
|
</div>
|
|
@@ -12036,7 +11886,7 @@ export function LoginForm() {
|
|
|
12036
11886
|
try {
|
|
12037
11887
|
const result = await authClient.signIn.email({
|
|
12038
11888
|
email,
|
|
12039
|
-
password
|
|
11889
|
+
password,
|
|
12040
11890
|
})
|
|
12041
11891
|
|
|
12042
11892
|
if (result.error) {
|
|
@@ -12056,9 +11906,7 @@ export function LoginForm() {
|
|
|
12056
11906
|
return (
|
|
12057
11907
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
12058
11908
|
{error && (
|
|
12059
|
-
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
|
|
12060
|
-
{error}
|
|
12061
|
-
</div>
|
|
11909
|
+
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">{error}</div>
|
|
12062
11910
|
)}
|
|
12063
11911
|
|
|
12064
11912
|
<div className="space-y-2">
|
|
@@ -12103,6 +11951,7 @@ export function LoginForm() {
|
|
|
12103
11951
|
function createUserDialogTemplate() {
|
|
12104
11952
|
return `'use client'
|
|
12105
11953
|
|
|
11954
|
+
import { createUser } from '@cms/actions/users'
|
|
12106
11955
|
import { Button } from '@cms/components/ui/button'
|
|
12107
11956
|
import {
|
|
12108
11957
|
Dialog,
|
|
@@ -12110,11 +11959,10 @@ import {
|
|
|
12110
11959
|
DialogDescription,
|
|
12111
11960
|
DialogHeader,
|
|
12112
11961
|
DialogTitle,
|
|
12113
|
-
DialogTrigger
|
|
11962
|
+
DialogTrigger,
|
|
12114
11963
|
} from '@cms/components/ui/dialog'
|
|
12115
11964
|
import { Input } from '@cms/components/ui/input'
|
|
12116
11965
|
import { Label } from '@cms/components/ui/label'
|
|
12117
|
-
import { createUser } from '@cms/actions/users'
|
|
12118
11966
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12119
11967
|
import { Loader2, UserPlus } from 'lucide-react'
|
|
12120
11968
|
import * as React from 'react'
|
|
@@ -12224,6 +12072,7 @@ export function CreateUserDialog() {
|
|
|
12224
12072
|
function editRoleDialogTemplate() {
|
|
12225
12073
|
return `'use client'
|
|
12226
12074
|
|
|
12075
|
+
import { updateUserRole } from '@cms/actions/users'
|
|
12227
12076
|
import { Button } from '@cms/components/ui/button'
|
|
12228
12077
|
import {
|
|
12229
12078
|
Dialog,
|
|
@@ -12231,7 +12080,7 @@ import {
|
|
|
12231
12080
|
DialogDescription,
|
|
12232
12081
|
DialogHeader,
|
|
12233
12082
|
DialogTitle,
|
|
12234
|
-
DialogTrigger
|
|
12083
|
+
DialogTrigger,
|
|
12235
12084
|
} from '@cms/components/ui/dialog'
|
|
12236
12085
|
import { Label } from '@cms/components/ui/label'
|
|
12237
12086
|
import {
|
|
@@ -12239,9 +12088,8 @@ import {
|
|
|
12239
12088
|
SelectContent,
|
|
12240
12089
|
SelectItem,
|
|
12241
12090
|
SelectTrigger,
|
|
12242
|
-
SelectValue
|
|
12091
|
+
SelectValue,
|
|
12243
12092
|
} from '@cms/components/ui/select'
|
|
12244
|
-
import { updateUserRole } from '@cms/actions/users'
|
|
12245
12093
|
import { UserRole } from '@cms/types/auth'
|
|
12246
12094
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12247
12095
|
import { Loader2 } from 'lucide-react'
|
|
@@ -12255,12 +12103,7 @@ interface EditRoleDialogProps {
|
|
|
12255
12103
|
children: React.ReactNode
|
|
12256
12104
|
}
|
|
12257
12105
|
|
|
12258
|
-
export function EditRoleDialog({
|
|
12259
|
-
userId,
|
|
12260
|
-
currentRole,
|
|
12261
|
-
userName,
|
|
12262
|
-
children
|
|
12263
|
-
}: EditRoleDialogProps) {
|
|
12106
|
+
export function EditRoleDialog({ userId, currentRole, userName, children }: EditRoleDialogProps) {
|
|
12264
12107
|
const [open, setOpen] = React.useState(false)
|
|
12265
12108
|
const [role, setRole] = React.useState(currentRole)
|
|
12266
12109
|
const [isPending, startTransition] = React.useTransition()
|
|
@@ -12289,9 +12132,7 @@ export function EditRoleDialog({
|
|
|
12289
12132
|
<DialogContent className="sm:max-w-[350px]">
|
|
12290
12133
|
<DialogHeader>
|
|
12291
12134
|
<DialogTitle>Edit Role</DialogTitle>
|
|
12292
|
-
<DialogDescription>
|
|
12293
|
-
Change the role for {userName}
|
|
12294
|
-
</DialogDescription>
|
|
12135
|
+
<DialogDescription>Change the role for {userName}</DialogDescription>
|
|
12295
12136
|
</DialogHeader>
|
|
12296
12137
|
<div className="space-y-4">
|
|
12297
12138
|
<div className="space-y-2">
|
|
@@ -12308,17 +12149,10 @@ export function EditRoleDialog({
|
|
|
12308
12149
|
</Select>
|
|
12309
12150
|
</div>
|
|
12310
12151
|
<div className="flex justify-end gap-2">
|
|
12311
|
-
<Button
|
|
12312
|
-
variant="outline"
|
|
12313
|
-
onClick={() => setOpen(false)}
|
|
12314
|
-
disabled={isPending}
|
|
12315
|
-
>
|
|
12152
|
+
<Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
|
12316
12153
|
Cancel
|
|
12317
12154
|
</Button>
|
|
12318
|
-
<Button
|
|
12319
|
-
onClick={handleSave}
|
|
12320
|
-
disabled={isPending || role === currentRole}
|
|
12321
|
-
>
|
|
12155
|
+
<Button onClick={handleSave} disabled={isPending || role === currentRole}>
|
|
12322
12156
|
{isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
|
|
12323
12157
|
Save
|
|
12324
12158
|
</Button>
|
|
@@ -12335,11 +12169,7 @@ export function EditRoleDialog({
|
|
|
12335
12169
|
function usersColumnsTemplate() {
|
|
12336
12170
|
return `'use client'
|
|
12337
12171
|
|
|
12338
|
-
import
|
|
12339
|
-
import {
|
|
12340
|
-
Avatar,
|
|
12341
|
-
AvatarFallback
|
|
12342
|
-
} from '@cms/components/ui/avatar'
|
|
12172
|
+
import { deleteUser } from '@cms/actions/users'
|
|
12343
12173
|
import {
|
|
12344
12174
|
AlertDialog,
|
|
12345
12175
|
AlertDialogAction,
|
|
@@ -12351,6 +12181,7 @@ import {
|
|
|
12351
12181
|
AlertDialogTitle,
|
|
12352
12182
|
AlertDialogTrigger,
|
|
12353
12183
|
} from '@cms/components/ui/alert-dialog'
|
|
12184
|
+
import { Avatar, AvatarFallback } from '@cms/components/ui/avatar'
|
|
12354
12185
|
import { Badge } from '@cms/components/ui/badge'
|
|
12355
12186
|
import { Button } from '@cms/components/ui/button'
|
|
12356
12187
|
import {
|
|
@@ -12359,14 +12190,14 @@ import {
|
|
|
12359
12190
|
DropdownMenuItem,
|
|
12360
12191
|
DropdownMenuLabel,
|
|
12361
12192
|
DropdownMenuSeparator,
|
|
12362
|
-
DropdownMenuTrigger
|
|
12193
|
+
DropdownMenuTrigger,
|
|
12363
12194
|
} from '@cms/components/ui/dropdown-menu'
|
|
12364
12195
|
import type { UserData } from '@cms/types/auth'
|
|
12365
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
12366
12196
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12197
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
12367
12198
|
import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
|
|
12199
|
+
import React from 'react'
|
|
12368
12200
|
import { toast } from 'sonner'
|
|
12369
|
-
import { deleteUser } from '@cms/actions/users'
|
|
12370
12201
|
import { EditRoleDialog } from './edit-role-dialog'
|
|
12371
12202
|
|
|
12372
12203
|
function getInitials(nameOrEmail: string): string {
|
|
@@ -12417,10 +12248,7 @@ function DeleteUserAction({
|
|
|
12417
12248
|
return (
|
|
12418
12249
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
12419
12250
|
<AlertDialogTrigger asChild>
|
|
12420
|
-
<DropdownMenuItem
|
|
12421
|
-
className="text-destructive"
|
|
12422
|
-
onSelect={(e) => e.preventDefault()}
|
|
12423
|
-
>
|
|
12251
|
+
<DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
|
|
12424
12252
|
<Trash className="size-4 mr-2" />
|
|
12425
12253
|
Delete user
|
|
12426
12254
|
</DropdownMenuItem>
|
|
@@ -12429,8 +12257,8 @@ function DeleteUserAction({
|
|
|
12429
12257
|
<AlertDialogHeader>
|
|
12430
12258
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
12431
12259
|
<AlertDialogDescription>
|
|
12432
|
-
This action cannot be undone. This will permanently delete{' '}
|
|
12433
|
-
|
|
12260
|
+
This action cannot be undone. This will permanently delete <strong>{userName}</strong>{' '}
|
|
12261
|
+
and all of their data.
|
|
12434
12262
|
</AlertDialogDescription>
|
|
12435
12263
|
</AlertDialogHeader>
|
|
12436
12264
|
<AlertDialogFooter>
|
|
@@ -12478,7 +12306,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12478
12306
|
</div>
|
|
12479
12307
|
</div>
|
|
12480
12308
|
)
|
|
12481
|
-
}
|
|
12309
|
+
},
|
|
12482
12310
|
},
|
|
12483
12311
|
{
|
|
12484
12312
|
accessorKey: 'emailVerified',
|
|
@@ -12490,7 +12318,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12490
12318
|
{verified ? 'Verified' : 'Unverified'}
|
|
12491
12319
|
</Badge>
|
|
12492
12320
|
)
|
|
12493
|
-
}
|
|
12321
|
+
},
|
|
12494
12322
|
},
|
|
12495
12323
|
{
|
|
12496
12324
|
accessorKey: 'role',
|
|
@@ -12520,7 +12348,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12520
12348
|
</Badge>
|
|
12521
12349
|
</EditRoleDialog>
|
|
12522
12350
|
)
|
|
12523
|
-
}
|
|
12351
|
+
},
|
|
12524
12352
|
},
|
|
12525
12353
|
{
|
|
12526
12354
|
accessorKey: 'createdAt',
|
|
@@ -12541,11 +12369,11 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12541
12369
|
{date.toLocaleDateString('en-US', {
|
|
12542
12370
|
month: 'short',
|
|
12543
12371
|
day: 'numeric',
|
|
12544
|
-
year: 'numeric'
|
|
12372
|
+
year: 'numeric',
|
|
12545
12373
|
})}
|
|
12546
12374
|
</div>
|
|
12547
12375
|
)
|
|
12548
|
-
}
|
|
12376
|
+
},
|
|
12549
12377
|
},
|
|
12550
12378
|
{
|
|
12551
12379
|
id: 'actions',
|
|
@@ -12564,9 +12392,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12564
12392
|
</DropdownMenuTrigger>
|
|
12565
12393
|
<DropdownMenuContent align="end">
|
|
12566
12394
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
12567
|
-
<DropdownMenuItem
|
|
12568
|
-
onClick={() => navigator.clipboard.writeText(row.original.id)}
|
|
12569
|
-
>
|
|
12395
|
+
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
|
|
12570
12396
|
Copy user ID
|
|
12571
12397
|
</DropdownMenuItem>
|
|
12572
12398
|
<DropdownMenuSeparator />
|
|
@@ -12579,8 +12405,8 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12579
12405
|
</DropdownMenu>
|
|
12580
12406
|
</div>
|
|
12581
12407
|
)
|
|
12582
|
-
}
|
|
12583
|
-
}
|
|
12408
|
+
},
|
|
12409
|
+
},
|
|
12584
12410
|
]
|
|
12585
12411
|
`;
|
|
12586
12412
|
}
|
|
@@ -12596,10 +12422,7 @@ export default function UsersPage() {
|
|
|
12596
12422
|
return (
|
|
12597
12423
|
<div className="flex flex-col">
|
|
12598
12424
|
<div className="flex items-center justify-between bg-card px-6 py-4 border-b">
|
|
12599
|
-
<PageHeader
|
|
12600
|
-
title="Users"
|
|
12601
|
-
description="Manage all CMS users"
|
|
12602
|
-
>
|
|
12425
|
+
<PageHeader title="Users">
|
|
12603
12426
|
<CreateUserDialog />
|
|
12604
12427
|
</PageHeader>
|
|
12605
12428
|
</div>
|
|
@@ -12616,6 +12439,18 @@ export default function UsersPage() {
|
|
|
12616
12439
|
function usersTableTemplate() {
|
|
12617
12440
|
return `'use client'
|
|
12618
12441
|
|
|
12442
|
+
import { authClient } from '@cms/auth/client'
|
|
12443
|
+
import { Button } from '@cms/components/ui/button'
|
|
12444
|
+
import {
|
|
12445
|
+
Table,
|
|
12446
|
+
TableBody,
|
|
12447
|
+
TableCell,
|
|
12448
|
+
TableHead,
|
|
12449
|
+
TableHeader,
|
|
12450
|
+
TableRow,
|
|
12451
|
+
} from '@cms/components/ui/table'
|
|
12452
|
+
import { useUsers } from '@cms/hooks/use-users'
|
|
12453
|
+
import type { UserData } from '@cms/types/auth'
|
|
12619
12454
|
import {
|
|
12620
12455
|
type ColumnDef,
|
|
12621
12456
|
type ColumnFiltersState,
|
|
@@ -12626,21 +12461,9 @@ import {
|
|
|
12626
12461
|
getSortedRowModel,
|
|
12627
12462
|
type SortingState,
|
|
12628
12463
|
useReactTable,
|
|
12629
|
-
type VisibilityState
|
|
12464
|
+
type VisibilityState,
|
|
12630
12465
|
} from '@tanstack/react-table'
|
|
12631
12466
|
import * as React from 'react'
|
|
12632
|
-
import { Button } from '@cms/components/ui/button'
|
|
12633
|
-
import {
|
|
12634
|
-
Table,
|
|
12635
|
-
TableBody,
|
|
12636
|
-
TableCell,
|
|
12637
|
-
TableHead,
|
|
12638
|
-
TableHeader,
|
|
12639
|
-
TableRow
|
|
12640
|
-
} from '@cms/components/ui/table'
|
|
12641
|
-
import { useUsers } from '@cms/hooks/use-users'
|
|
12642
|
-
import { authClient } from '@cms/auth/client'
|
|
12643
|
-
import type { UserData } from '@cms/types/auth'
|
|
12644
12467
|
|
|
12645
12468
|
interface UsersTableProps<TValue> {
|
|
12646
12469
|
columns: ColumnDef<UserData, TValue>[]
|
|
@@ -12670,11 +12493,11 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
|
|
|
12670
12493
|
email: session.user.email,
|
|
12671
12494
|
name: session.user.name,
|
|
12672
12495
|
image: session.user.image,
|
|
12673
|
-
role: (session.user as { role?: string }).role || 'member'
|
|
12496
|
+
role: (session.user as { role?: string }).role || 'member',
|
|
12674
12497
|
}
|
|
12675
|
-
: null
|
|
12498
|
+
: null,
|
|
12676
12499
|
},
|
|
12677
|
-
state: { sorting, columnFilters, columnVisibility }
|
|
12500
|
+
state: { sorting, columnFilters, columnVisibility },
|
|
12678
12501
|
})
|
|
12679
12502
|
|
|
12680
12503
|
return (
|
|
@@ -13614,32 +13437,37 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
|
|
|
13614
13437
|
const { execFile } = await import("child_process");
|
|
13615
13438
|
const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
|
|
13616
13439
|
const runSeed2 = (overwrite) => new Promise((resolve, reject) => {
|
|
13617
|
-
execFile(
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
}
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13440
|
+
execFile(
|
|
13441
|
+
tsxBin,
|
|
13442
|
+
[seedPath],
|
|
13443
|
+
{
|
|
13444
|
+
cwd,
|
|
13445
|
+
env: {
|
|
13446
|
+
...process.env,
|
|
13447
|
+
SEED_EMAIL: email,
|
|
13448
|
+
SEED_PASSWORD: password3,
|
|
13449
|
+
SEED_NAME: name || "Admin",
|
|
13450
|
+
...overwrite ? { SEED_OVERWRITE: "true" } : {}
|
|
13451
|
+
}
|
|
13452
|
+
},
|
|
13453
|
+
(err, stdout, stderr) => {
|
|
13454
|
+
if (err && "code" in err && err.code === 2) {
|
|
13455
|
+
resolve({ code: 2, stdout });
|
|
13456
|
+
} else if (err) {
|
|
13457
|
+
reject(new Error(stderr || err.message));
|
|
13458
|
+
} else {
|
|
13459
|
+
resolve({ code: 0, stdout });
|
|
13460
|
+
}
|
|
13633
13461
|
}
|
|
13634
|
-
|
|
13462
|
+
);
|
|
13635
13463
|
});
|
|
13636
|
-
const
|
|
13637
|
-
|
|
13464
|
+
const spinner5 = clack.spinner();
|
|
13465
|
+
spinner5.start("Creating admin user...");
|
|
13638
13466
|
try {
|
|
13639
13467
|
const result = await runSeed2(false);
|
|
13640
13468
|
if (result.code === 2) {
|
|
13641
13469
|
const existingName = result.stdout.split("\n").find((l) => l.startsWith("EXISTING_USER:"))?.replace("EXISTING_USER:", "")?.trim() || "unknown";
|
|
13642
|
-
|
|
13470
|
+
spinner5.stop(`Account already exists for ${email}`);
|
|
13643
13471
|
const overwrite = await clack.confirm({
|
|
13644
13472
|
message: `An admin account (${existingName}) already exists with this email. Replace it?`
|
|
13645
13473
|
});
|
|
@@ -13651,14 +13479,14 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
|
|
|
13651
13479
|
}
|
|
13652
13480
|
process.exit(0);
|
|
13653
13481
|
}
|
|
13654
|
-
|
|
13482
|
+
spinner5.start("Replacing admin user...");
|
|
13655
13483
|
await runSeed2(true);
|
|
13656
|
-
|
|
13484
|
+
spinner5.stop("Admin user replaced");
|
|
13657
13485
|
} else {
|
|
13658
|
-
|
|
13486
|
+
spinner5.stop("Admin user created");
|
|
13659
13487
|
}
|
|
13660
13488
|
} catch (err) {
|
|
13661
|
-
|
|
13489
|
+
spinner5.stop("Failed to create admin user");
|
|
13662
13490
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
13663
13491
|
clack.log.error(errMsg);
|
|
13664
13492
|
clack.log.info("You can run the seed script manually:");
|
|
@@ -13682,7 +13510,7 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
|
|
|
13682
13510
|
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(
|
|
13683
13511
|
"--database-url <url>",
|
|
13684
13512
|
"PostgreSQL database connection string (postgres:// or postgresql://)"
|
|
13685
|
-
).action(
|
|
13513
|
+
).option("--force", "Overwrite all existing CMS files (nuclear option)").action(
|
|
13686
13514
|
async (name, options) => {
|
|
13687
13515
|
p4.intro(pc2.bgCyan(pc2.black(" BetterStart CMS ")));
|
|
13688
13516
|
let cwd = process.cwd();
|
|
@@ -13691,21 +13519,45 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13691
13519
|
let isFreshProject = false;
|
|
13692
13520
|
let srcDir;
|
|
13693
13521
|
if (project.isExisting) {
|
|
13694
|
-
p4.log.info(`
|
|
13695
|
-
p4.log.info(`Package manager: ${pc2.cyan(pm)}`);
|
|
13522
|
+
p4.log.info(`Next.js project detected ${pc2.dim("\xB7")} ${pc2.cyan(pm)}`);
|
|
13696
13523
|
srcDir = project.hasSrcDir;
|
|
13697
13524
|
if (!project.hasTypeScript) {
|
|
13698
13525
|
p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
|
|
13699
13526
|
process.exit(1);
|
|
13700
13527
|
}
|
|
13701
|
-
if (
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13528
|
+
if (options.force) {
|
|
13529
|
+
const nukeDirs = ["cms", "app/(cms)"];
|
|
13530
|
+
const nukeFiles = ["cms.config.ts", "CMS.md", "drizzle.config.ts"];
|
|
13531
|
+
let nuked = 0;
|
|
13532
|
+
for (const dir of nukeDirs) {
|
|
13533
|
+
const fullPath = path37.resolve(cwd, dir);
|
|
13534
|
+
if (fs32.existsSync(fullPath)) {
|
|
13535
|
+
fs32.rmSync(fullPath, { recursive: true, force: true });
|
|
13536
|
+
nuked++;
|
|
13537
|
+
}
|
|
13538
|
+
}
|
|
13539
|
+
for (const file of nukeFiles) {
|
|
13540
|
+
const fullPath = path37.resolve(cwd, file);
|
|
13541
|
+
if (fs32.existsSync(fullPath)) {
|
|
13542
|
+
fs32.unlinkSync(fullPath);
|
|
13543
|
+
nuked++;
|
|
13544
|
+
}
|
|
13705
13545
|
}
|
|
13546
|
+
if (nuked > 0) {
|
|
13547
|
+
p4.log.warn(`${pc2.yellow("Force mode:")} removed ${nuked} existing CMS paths`);
|
|
13548
|
+
}
|
|
13549
|
+
project = detectProject(cwd);
|
|
13550
|
+
} else if (project.conflicts.length > 0) {
|
|
13551
|
+
const conflictLines = project.conflicts.map((c) => `${pc2.yellow("\u25B2")} ${c}`);
|
|
13552
|
+
conflictLines.push(
|
|
13553
|
+
"",
|
|
13554
|
+
pc2.dim("Existing files will not be overwritten."),
|
|
13555
|
+
pc2.dim(`Use ${pc2.bold("--force")} to remove existing CMS files before scaffolding.`)
|
|
13556
|
+
);
|
|
13557
|
+
p4.note(conflictLines.join("\n"), pc2.yellow("Conflicts"));
|
|
13706
13558
|
if (!options.yes) {
|
|
13707
13559
|
const proceed = await p4.confirm({
|
|
13708
|
-
message: "Continue anyway?
|
|
13560
|
+
message: "Continue anyway?",
|
|
13709
13561
|
initialValue: true
|
|
13710
13562
|
});
|
|
13711
13563
|
if (p4.isCancel(proceed) || !proceed) {
|
|
@@ -13814,80 +13666,91 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13814
13666
|
...getDefaultConfig(srcDir),
|
|
13815
13667
|
features: { email: features.includeEmail }
|
|
13816
13668
|
};
|
|
13669
|
+
const results = [];
|
|
13817
13670
|
const s = p4.spinner();
|
|
13818
|
-
s.start("
|
|
13671
|
+
s.start("Directory structure");
|
|
13819
13672
|
const baseFiles = scaffoldBase({ cwd, config });
|
|
13820
|
-
|
|
13821
|
-
s.
|
|
13673
|
+
results.push({ label: "Directory structure", result: `${baseFiles.length} files` });
|
|
13674
|
+
s.message("TypeScript aliases");
|
|
13822
13675
|
const tsResult = scaffoldTsconfig(cwd);
|
|
13823
|
-
|
|
13824
|
-
|
|
13676
|
+
results.push({
|
|
13677
|
+
label: "TypeScript aliases",
|
|
13678
|
+
result: tsResult.added.length > 0 ? `${tsResult.added.length} paths` : "already set"
|
|
13679
|
+
});
|
|
13680
|
+
s.message("Tailwind CSS");
|
|
13825
13681
|
const twResult = scaffoldTailwind(cwd, srcDir);
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
s.stop("No CSS file found (will configure later)");
|
|
13832
|
-
}
|
|
13833
|
-
s.start("Setting up environment variables...");
|
|
13682
|
+
results.push({
|
|
13683
|
+
label: "Tailwind CSS",
|
|
13684
|
+
result: twResult.appended ? "updated" : twResult.file ? "already set" : "no CSS file"
|
|
13685
|
+
});
|
|
13686
|
+
s.message("Environment variables");
|
|
13834
13687
|
const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail, databaseUrl });
|
|
13835
|
-
const
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13688
|
+
const envCount = envResult.added.length + envResult.updated.length;
|
|
13689
|
+
results.push({
|
|
13690
|
+
label: "Environment variables",
|
|
13691
|
+
result: envCount > 0 ? `${envCount} vars` : "already set"
|
|
13692
|
+
});
|
|
13693
|
+
s.message("Database");
|
|
13839
13694
|
const dbFiles = scaffoldDatabase({ cwd, config });
|
|
13840
|
-
|
|
13695
|
+
results.push({ label: "Database", result: `${dbFiles.length} files` });
|
|
13696
|
+
s.message("Authentication");
|
|
13697
|
+
const authFiles = scaffoldAuth({ cwd, config });
|
|
13698
|
+
results.push({ label: "Authentication", result: `${authFiles.length} files` });
|
|
13699
|
+
s.message("Components");
|
|
13700
|
+
const compFiles = scaffoldComponents({ cwd, config });
|
|
13701
|
+
results.push({ label: "Components", result: `${compFiles.length} files` });
|
|
13702
|
+
s.message("Pages & layouts");
|
|
13703
|
+
const layoutFiles = scaffoldLayout({ cwd, config });
|
|
13704
|
+
results.push({ label: "Pages & layouts", result: `${layoutFiles.length} files` });
|
|
13705
|
+
s.message("API routes");
|
|
13706
|
+
const apiFiles = scaffoldApiRoutes({ cwd, config });
|
|
13707
|
+
results.push({ label: "API routes", result: `${apiFiles.length} routes` });
|
|
13708
|
+
s.message("Linter");
|
|
13709
|
+
let linterResult;
|
|
13710
|
+
if (project.linter.type === "none") {
|
|
13711
|
+
const biomeResult = scaffoldBiome(cwd, project.linter);
|
|
13712
|
+
linterResult = biomeResult.installed ? "biome (new)" : "none";
|
|
13713
|
+
} else {
|
|
13714
|
+
linterResult = project.linter.type;
|
|
13715
|
+
}
|
|
13716
|
+
results.push({ label: "Linter", result: linterResult });
|
|
13717
|
+
const maxLabel = Math.max(...results.map((r) => r.label.length));
|
|
13718
|
+
const noteLines = results.map((r) => {
|
|
13719
|
+
const padded = r.label.padEnd(maxLabel + 3);
|
|
13720
|
+
return `${pc2.green("\u2713")} ${padded}${pc2.dim(r.result)}`;
|
|
13721
|
+
});
|
|
13722
|
+
s.stop("");
|
|
13723
|
+
process.stdout.write("\x1B[2A\x1B[J");
|
|
13724
|
+
p4.note(noteLines.join("\n"), "Scaffolded CMS");
|
|
13841
13725
|
const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
|
|
13842
13726
|
if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
|
|
13843
|
-
if (
|
|
13727
|
+
if (options.force) {
|
|
13728
|
+
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
|
|
13729
|
+
fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
|
|
13730
|
+
p4.log.success("Updated drizzle.config.ts");
|
|
13731
|
+
} else if (!options.yes) {
|
|
13844
13732
|
const overwrite = await p4.confirm({
|
|
13845
13733
|
message: "drizzle.config.ts already exists. Overwrite with latest version?",
|
|
13846
13734
|
initialValue: true
|
|
13847
13735
|
});
|
|
13848
13736
|
if (!p4.isCancel(overwrite) && overwrite) {
|
|
13849
|
-
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-
|
|
13737
|
+
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
|
|
13850
13738
|
fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
|
|
13851
13739
|
p4.log.success("Updated drizzle.config.ts");
|
|
13852
13740
|
}
|
|
13853
13741
|
}
|
|
13854
13742
|
}
|
|
13855
|
-
s.start("
|
|
13856
|
-
const authFiles = scaffoldAuth({ cwd, config });
|
|
13857
|
-
s.stop(`Created ${authFiles.length} auth files`);
|
|
13858
|
-
s.start("Copying CMS components...");
|
|
13859
|
-
const compFiles = scaffoldComponents({ cwd, config });
|
|
13860
|
-
s.stop(`Created ${compFiles.length} component files`);
|
|
13861
|
-
s.start("Creating CMS pages and layouts...");
|
|
13862
|
-
const layoutFiles = scaffoldLayout({ cwd, config });
|
|
13863
|
-
s.stop(`Created ${layoutFiles.length} page files`);
|
|
13864
|
-
s.start("Creating API routes...");
|
|
13865
|
-
const apiFiles = scaffoldApiRoutes({ cwd, config });
|
|
13866
|
-
s.stop(`Created ${apiFiles.length} API routes`);
|
|
13867
|
-
s.start("Checking for linter...");
|
|
13868
|
-
if (project.linter.type === "none") {
|
|
13869
|
-
s.stop("No linter found");
|
|
13870
|
-
s.start("Setting up Biome linter...");
|
|
13871
|
-
const biomeResult = scaffoldBiome(cwd, project.linter);
|
|
13872
|
-
if (biomeResult.installed) {
|
|
13873
|
-
s.stop("Created biome.json");
|
|
13874
|
-
} else {
|
|
13875
|
-
s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
|
|
13876
|
-
}
|
|
13877
|
-
} else {
|
|
13878
|
-
s.stop(`Linter: ${pc2.cyan(project.linter.type)} (${project.linter.configFile})`);
|
|
13879
|
-
}
|
|
13880
|
-
s.start("Installing dependencies (this may take a minute)...");
|
|
13743
|
+
s.start("Installing dependencies (this may take a minute)");
|
|
13881
13744
|
const depsResult = await installDependenciesAsync({
|
|
13882
13745
|
cwd,
|
|
13883
13746
|
pm,
|
|
13884
13747
|
includeEmail: features.includeEmail,
|
|
13885
13748
|
includeBiome: project.linter.type === "none"
|
|
13886
13749
|
});
|
|
13750
|
+
let depsInstalled = false;
|
|
13887
13751
|
if (depsResult.success) {
|
|
13888
|
-
s.stop(
|
|
13889
|
-
|
|
13890
|
-
);
|
|
13752
|
+
s.stop("");
|
|
13753
|
+
depsInstalled = true;
|
|
13891
13754
|
} else {
|
|
13892
13755
|
s.stop("Failed to install dependencies");
|
|
13893
13756
|
p4.log.warning(depsResult.error ?? "Unknown error");
|
|
@@ -13897,24 +13760,59 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13897
13760
|
${pc2.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
|
|
13898
13761
|
);
|
|
13899
13762
|
}
|
|
13900
|
-
|
|
13763
|
+
if (depsInstalled) {
|
|
13764
|
+
process.stdout.write("\x1B[2A\x1B[J");
|
|
13765
|
+
}
|
|
13766
|
+
s.start(`Applying ${features.preset} preset`);
|
|
13901
13767
|
const presetResult = scaffoldPreset({ cwd, config, preset: features.preset });
|
|
13768
|
+
{
|
|
13769
|
+
const entityNames = [];
|
|
13770
|
+
const formNames = [];
|
|
13771
|
+
const schemasDir = path37.join(cwd, config.paths.schemas);
|
|
13772
|
+
const formsDir = path37.join(schemasDir, "forms");
|
|
13773
|
+
if (fs32.existsSync(schemasDir)) {
|
|
13774
|
+
for (const f of fs32.readdirSync(schemasDir)) {
|
|
13775
|
+
if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
|
|
13776
|
+
}
|
|
13777
|
+
}
|
|
13778
|
+
if (fs32.existsSync(formsDir)) {
|
|
13779
|
+
for (const f of fs32.readdirSync(formsDir)) {
|
|
13780
|
+
if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
|
|
13781
|
+
}
|
|
13782
|
+
}
|
|
13783
|
+
regenerateCmsDoc(cwd, config, {
|
|
13784
|
+
preset: features.preset,
|
|
13785
|
+
schemas: entityNames,
|
|
13786
|
+
forms: formNames
|
|
13787
|
+
});
|
|
13788
|
+
}
|
|
13789
|
+
s.stop("");
|
|
13790
|
+
process.stdout.write("\x1B[2A\x1B[J");
|
|
13791
|
+
const installLines = [];
|
|
13792
|
+
if (depsInstalled) {
|
|
13793
|
+
installLines.push(
|
|
13794
|
+
`${pc2.green("\u2713")} Dependencies ${pc2.dim(`${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`)}`
|
|
13795
|
+
);
|
|
13796
|
+
}
|
|
13902
13797
|
if (presetResult.errors.length > 0) {
|
|
13903
|
-
|
|
13798
|
+
installLines.push(
|
|
13799
|
+
`${pc2.yellow("\u25B2")} Preset ${pc2.dim(`${features.preset} \u2014 ${presetResult.errors.length} warning(s)`)}`
|
|
13800
|
+
);
|
|
13904
13801
|
for (const err of presetResult.errors) {
|
|
13905
|
-
|
|
13802
|
+
installLines.push(` ${pc2.dim(err)}`);
|
|
13906
13803
|
}
|
|
13907
13804
|
} else {
|
|
13908
|
-
|
|
13909
|
-
|
|
13805
|
+
installLines.push(
|
|
13806
|
+
`${pc2.green("\u2713")} Preset ${pc2.dim(`${features.preset} \u2014 ${presetResult.schemas.length} schemas, ${presetResult.generatedFiles.length} files`)}`
|
|
13910
13807
|
);
|
|
13911
13808
|
}
|
|
13809
|
+
p4.note(installLines.join("\n"), "Installed");
|
|
13912
13810
|
let dbPushed = false;
|
|
13913
13811
|
if (depsResult.success && hasDbUrl(cwd)) {
|
|
13914
|
-
s.start("Pushing database schema (drizzle-kit push)
|
|
13812
|
+
s.start("Pushing database schema (drizzle-kit push)");
|
|
13915
13813
|
const pushResult = await runDrizzlePush(cwd);
|
|
13916
13814
|
if (pushResult.success) {
|
|
13917
|
-
s.stop("Database schema pushed
|
|
13815
|
+
s.stop(`${pc2.green("\u2713")} Database schema pushed`);
|
|
13918
13816
|
dbPushed = true;
|
|
13919
13817
|
} else {
|
|
13920
13818
|
s.stop("Database push failed");
|
|
@@ -13926,66 +13824,73 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13926
13824
|
let seedPassword;
|
|
13927
13825
|
let seedSuccess = false;
|
|
13928
13826
|
if (dbPushed && !options.yes) {
|
|
13929
|
-
p4.
|
|
13930
|
-
const
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
|
|
13827
|
+
p4.note(pc2.dim("Create your first admin user to access the CMS."), "Admin account");
|
|
13828
|
+
const credentials = await p4.group(
|
|
13829
|
+
{
|
|
13830
|
+
email: () => p4.text({
|
|
13831
|
+
message: "Admin email",
|
|
13832
|
+
placeholder: "admin@example.com",
|
|
13833
|
+
validate: (v) => {
|
|
13834
|
+
if (!v || !v.includes("@")) return "Please enter a valid email";
|
|
13835
|
+
}
|
|
13836
|
+
}),
|
|
13837
|
+
password: () => p4.password({
|
|
13838
|
+
message: "Admin password",
|
|
13839
|
+
validate: (v) => {
|
|
13840
|
+
if (!v || v.length < 8) return "Password must be at least 8 characters";
|
|
13841
|
+
}
|
|
13842
|
+
})
|
|
13843
|
+
},
|
|
13844
|
+
{
|
|
13845
|
+
onCancel: () => {
|
|
13846
|
+
p4.cancel("Setup cancelled.");
|
|
13847
|
+
process.exit(0);
|
|
13848
|
+
}
|
|
13935
13849
|
}
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13850
|
+
);
|
|
13851
|
+
seedEmail = credentials.email;
|
|
13852
|
+
seedPassword = credentials.password;
|
|
13853
|
+
s.start("Creating admin user");
|
|
13854
|
+
let seedResult = await runSeed(
|
|
13855
|
+
cwd,
|
|
13856
|
+
config.paths?.cms ?? "./cms",
|
|
13857
|
+
credentials.email,
|
|
13858
|
+
credentials.password
|
|
13859
|
+
);
|
|
13860
|
+
if (seedResult.existingUser) {
|
|
13861
|
+
s.stop(`${pc2.yellow("\u25B2")} Admin user already exists (${seedResult.existingUser})`);
|
|
13862
|
+
const replace = await p4.confirm({
|
|
13863
|
+
message: "Replace existing admin user?",
|
|
13864
|
+
initialValue: false
|
|
13865
|
+
});
|
|
13866
|
+
if (!p4.isCancel(replace) && replace) {
|
|
13867
|
+
s.start("Replacing admin user");
|
|
13868
|
+
seedResult = await runSeed(
|
|
13869
|
+
cwd,
|
|
13870
|
+
config.paths?.cms ?? "./cms",
|
|
13871
|
+
credentials.email,
|
|
13872
|
+
credentials.password,
|
|
13873
|
+
true
|
|
13874
|
+
);
|
|
13875
|
+
} else {
|
|
13876
|
+
seedSuccess = true;
|
|
13945
13877
|
}
|
|
13946
|
-
});
|
|
13947
|
-
if (p4.isCancel(password3)) {
|
|
13948
|
-
p4.cancel("Setup cancelled.");
|
|
13949
|
-
process.exit(0);
|
|
13950
13878
|
}
|
|
13951
|
-
seedEmail = email;
|
|
13952
|
-
seedPassword = password3;
|
|
13953
|
-
s.start("Creating admin user...");
|
|
13954
|
-
const seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", email, password3);
|
|
13955
13879
|
if (seedResult.success) {
|
|
13956
|
-
s.stop("Admin user created
|
|
13880
|
+
s.stop(`${pc2.green("\u2713")} Admin user created`);
|
|
13957
13881
|
seedSuccess = true;
|
|
13958
|
-
} else {
|
|
13959
|
-
s.stop("Failed to create admin user
|
|
13960
|
-
p4.
|
|
13961
|
-
|
|
13962
|
-
|
|
13963
|
-
|
|
13964
|
-
|
|
13965
|
-
|
|
13966
|
-
const entityNames = [];
|
|
13967
|
-
const formNames = [];
|
|
13968
|
-
const schemasDir = path37.join(cwd, config.paths.schemas);
|
|
13969
|
-
const formsDir = path37.join(schemasDir, "forms");
|
|
13970
|
-
if (fs32.existsSync(schemasDir)) {
|
|
13971
|
-
for (const f of fs32.readdirSync(schemasDir)) {
|
|
13972
|
-
if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
|
|
13973
|
-
}
|
|
13974
|
-
}
|
|
13975
|
-
if (fs32.existsSync(formsDir)) {
|
|
13976
|
-
for (const f of fs32.readdirSync(formsDir)) {
|
|
13977
|
-
if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
|
|
13978
|
-
}
|
|
13882
|
+
} else if (!seedSuccess && seedResult.error) {
|
|
13883
|
+
s.stop(`${pc2.red("\u2717")} Failed to create admin user`);
|
|
13884
|
+
p4.note(
|
|
13885
|
+
`${pc2.red(seedResult.error)}
|
|
13886
|
+
|
|
13887
|
+
Run manually: ${pc2.cyan("npx betterstart seed")}`,
|
|
13888
|
+
pc2.red("Seed failed")
|
|
13889
|
+
);
|
|
13979
13890
|
}
|
|
13980
|
-
regenerateCmsDoc(cwd, config, {
|
|
13981
|
-
preset: features.preset,
|
|
13982
|
-
schemas: entityNames,
|
|
13983
|
-
forms: formNames
|
|
13984
|
-
});
|
|
13985
13891
|
}
|
|
13986
|
-
s.stop("Generated CMS.md");
|
|
13987
13892
|
if (isFreshProject) {
|
|
13988
|
-
s.start("Creating initial git commit
|
|
13893
|
+
s.start("Creating initial git commit");
|
|
13989
13894
|
try {
|
|
13990
13895
|
execFileSync4("git", ["init"], { cwd, stdio: "pipe" });
|
|
13991
13896
|
execFileSync4("git", ["add", "."], { cwd, stdio: "pipe" });
|
|
@@ -14001,7 +13906,6 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
14001
13906
|
const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
|
|
14002
13907
|
const summaryLines = [
|
|
14003
13908
|
`Preset: ${pc2.cyan(features.preset)}`,
|
|
14004
|
-
`Email: ${features.includeEmail ? pc2.green("yes") : pc2.dim("no")}`,
|
|
14005
13909
|
`Files created: ${pc2.cyan(String(totalFiles))}`,
|
|
14006
13910
|
`Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`
|
|
14007
13911
|
];
|
|
@@ -14091,26 +13995,14 @@ function hasDbUrl(cwd) {
|
|
|
14091
13995
|
}
|
|
14092
13996
|
return false;
|
|
14093
13997
|
}
|
|
14094
|
-
|
|
13998
|
+
function runSeed(cwd, cmsDir, email, password3, overwrite = false) {
|
|
14095
13999
|
const scriptsDir = path37.join(cwd, cmsDir, "scripts");
|
|
14096
14000
|
const seedPath = path37.join(scriptsDir, "seed.ts");
|
|
14097
14001
|
if (!fs32.existsSync(scriptsDir)) {
|
|
14098
14002
|
fs32.mkdirSync(scriptsDir, { recursive: true });
|
|
14099
14003
|
}
|
|
14100
14004
|
fs32.writeFileSync(seedPath, buildSeedScript(), "utf-8");
|
|
14101
|
-
|
|
14102
|
-
const tsxBin = path37.join(cwd, "node_modules", ".bin", "tsx");
|
|
14103
|
-
execFileSync4(tsxBin, [seedPath], {
|
|
14104
|
-
cwd,
|
|
14105
|
-
stdio: "pipe",
|
|
14106
|
-
timeout: 3e4,
|
|
14107
|
-
env: { ...process.env, SEED_EMAIL: email, SEED_PASSWORD: password3, SEED_NAME: "Admin" }
|
|
14108
|
-
});
|
|
14109
|
-
return { success: true, error: null };
|
|
14110
|
-
} catch (err) {
|
|
14111
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
14112
|
-
return { success: false, error: msg };
|
|
14113
|
-
} finally {
|
|
14005
|
+
const cleanup = () => {
|
|
14114
14006
|
try {
|
|
14115
14007
|
fs32.unlinkSync(seedPath);
|
|
14116
14008
|
if (fs32.existsSync(scriptsDir) && fs32.readdirSync(scriptsDir).length === 0) {
|
|
@@ -14118,7 +14010,70 @@ async function runSeed(cwd, cmsDir, email, password3) {
|
|
|
14118
14010
|
}
|
|
14119
14011
|
} catch {
|
|
14120
14012
|
}
|
|
14121
|
-
}
|
|
14013
|
+
};
|
|
14014
|
+
return new Promise((resolve) => {
|
|
14015
|
+
const tsxBin = path37.join(cwd, "node_modules", ".bin", "tsx");
|
|
14016
|
+
const child = spawn2(tsxBin, [seedPath], {
|
|
14017
|
+
cwd,
|
|
14018
|
+
stdio: "pipe",
|
|
14019
|
+
env: {
|
|
14020
|
+
...process.env,
|
|
14021
|
+
SEED_EMAIL: email,
|
|
14022
|
+
SEED_PASSWORD: password3,
|
|
14023
|
+
SEED_NAME: "Admin",
|
|
14024
|
+
...overwrite ? { SEED_OVERWRITE: "true" } : {}
|
|
14025
|
+
}
|
|
14026
|
+
});
|
|
14027
|
+
let stdout = "";
|
|
14028
|
+
let stderr = "";
|
|
14029
|
+
child.stdout?.on("data", (chunk) => {
|
|
14030
|
+
stdout += chunk.toString();
|
|
14031
|
+
});
|
|
14032
|
+
child.stderr?.on("data", (chunk) => {
|
|
14033
|
+
stderr += chunk.toString();
|
|
14034
|
+
});
|
|
14035
|
+
const timeout = setTimeout(() => {
|
|
14036
|
+
child.kill();
|
|
14037
|
+
cleanup();
|
|
14038
|
+
resolve({ success: false, error: "Seed timed out after 30 seconds" });
|
|
14039
|
+
}, 3e4);
|
|
14040
|
+
child.on("close", (code) => {
|
|
14041
|
+
clearTimeout(timeout);
|
|
14042
|
+
cleanup();
|
|
14043
|
+
if (code === 0) {
|
|
14044
|
+
resolve({ success: true, error: null });
|
|
14045
|
+
} else if (code === 2) {
|
|
14046
|
+
const name = stdout.match(/EXISTING_USER:(.+)/)?.[1]?.trim();
|
|
14047
|
+
resolve({ success: false, error: null, existingUser: name ?? email });
|
|
14048
|
+
} else {
|
|
14049
|
+
resolve({ success: false, error: parseSeedError(stdout, stderr) });
|
|
14050
|
+
}
|
|
14051
|
+
});
|
|
14052
|
+
child.on("error", (err) => {
|
|
14053
|
+
clearTimeout(timeout);
|
|
14054
|
+
cleanup();
|
|
14055
|
+
resolve({ success: false, error: parseSeedError("", err.message) });
|
|
14056
|
+
});
|
|
14057
|
+
});
|
|
14058
|
+
}
|
|
14059
|
+
function parseSeedError(stdout, stderr) {
|
|
14060
|
+
const combined = `${stdout}
|
|
14061
|
+
${stderr}`;
|
|
14062
|
+
const seedFailed = combined.match(/Seed failed:\s*(.+)/)?.[1]?.trim();
|
|
14063
|
+
if (seedFailed) return seedFailed;
|
|
14064
|
+
if (combined.includes("Failed to create user")) return "Auth API failed to create user";
|
|
14065
|
+
if (combined.includes("ECONNREFUSED") || combined.includes("connection refused"))
|
|
14066
|
+
return "Could not connect to database";
|
|
14067
|
+
if (combined.includes("BETTERSTART_DATABASE_URL")) return "Database URL is missing or invalid";
|
|
14068
|
+
if (combined.includes("password authentication failed"))
|
|
14069
|
+
return "Database authentication failed \u2014 check your connection string";
|
|
14070
|
+
if (combined.includes("does not exist") && combined.includes("relation"))
|
|
14071
|
+
return "Database tables not found \u2014 run npx drizzle-kit push first";
|
|
14072
|
+
if (combined.includes("MODULE_NOT_FOUND") || combined.includes("Cannot find module"))
|
|
14073
|
+
return "Missing dependencies \u2014 run your package manager install first";
|
|
14074
|
+
const firstLine = stderr.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("at ") && !l.startsWith("node:"));
|
|
14075
|
+
if (firstLine) return firstLine;
|
|
14076
|
+
return "Unknown error \u2014 run npx betterstart seed for details";
|
|
14122
14077
|
}
|
|
14123
14078
|
function runDrizzlePush(cwd) {
|
|
14124
14079
|
return new Promise((resolve) => {
|
|
@@ -14364,41 +14319,12 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
|
|
|
14364
14319
|
console.log("");
|
|
14365
14320
|
});
|
|
14366
14321
|
|
|
14367
|
-
// src/commands/update-deps.ts
|
|
14368
|
-
import path39 from "path";
|
|
14369
|
-
import * as clack2 from "@clack/prompts";
|
|
14370
|
-
import { Command as Command5 } from "commander";
|
|
14371
|
-
var updateDepsCommand = new Command5("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
|
|
14372
|
-
const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
|
|
14373
|
-
clack2.intro("BetterStart Update Dependencies");
|
|
14374
|
-
const pm = detectPackageManager(cwd);
|
|
14375
|
-
clack2.log.info(`Package manager: ${pm}`);
|
|
14376
|
-
const config = await resolveConfig(cwd);
|
|
14377
|
-
const includeEmail = config.features?.email ?? true;
|
|
14378
|
-
const s = clack2.spinner();
|
|
14379
|
-
s.start("Installing dependencies...");
|
|
14380
|
-
const result = await installDependenciesAsync({
|
|
14381
|
-
cwd,
|
|
14382
|
-
pm,
|
|
14383
|
-
includeEmail,
|
|
14384
|
-
includeBiome: false
|
|
14385
|
-
});
|
|
14386
|
-
if (result.success) {
|
|
14387
|
-
s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
|
|
14388
|
-
} else {
|
|
14389
|
-
s.stop("Dependency install failed");
|
|
14390
|
-
clack2.log.error(result.error ?? "Unknown error");
|
|
14391
|
-
process.exit(1);
|
|
14392
|
-
}
|
|
14393
|
-
clack2.outro("Dependencies updated");
|
|
14394
|
-
});
|
|
14395
|
-
|
|
14396
14322
|
// src/commands/uninstall.ts
|
|
14397
14323
|
import fs35 from "fs";
|
|
14398
|
-
import
|
|
14324
|
+
import path39 from "path";
|
|
14399
14325
|
import * as p5 from "@clack/prompts";
|
|
14326
|
+
import { Command as Command5 } from "commander";
|
|
14400
14327
|
import pc3 from "picocolors";
|
|
14401
|
-
import { Command as Command6 } from "commander";
|
|
14402
14328
|
|
|
14403
14329
|
// src/commands/uninstall-cleaners.ts
|
|
14404
14330
|
import fs34 from "fs";
|
|
@@ -14455,7 +14381,7 @@ function cleanTsconfig(tsconfigPath) {
|
|
|
14455
14381
|
}
|
|
14456
14382
|
if (removed.length === 0) return [];
|
|
14457
14383
|
if (Object.keys(paths).length === 0) {
|
|
14458
|
-
|
|
14384
|
+
compilerOptions.paths = void 0;
|
|
14459
14385
|
} else {
|
|
14460
14386
|
compilerOptions.paths = paths;
|
|
14461
14387
|
}
|
|
@@ -14509,14 +14435,14 @@ function cleanEnvFile(envPath) {
|
|
|
14509
14435
|
}
|
|
14510
14436
|
if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
|
|
14511
14437
|
const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
|
|
14512
|
-
if (nextNonEmpty
|
|
14438
|
+
if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
|
|
14513
14439
|
continue;
|
|
14514
14440
|
}
|
|
14515
14441
|
}
|
|
14516
14442
|
kept.push(line);
|
|
14517
14443
|
}
|
|
14518
14444
|
if (removed.length === 0) return [];
|
|
14519
|
-
|
|
14445
|
+
const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
14520
14446
|
if (result === "") {
|
|
14521
14447
|
fs34.unlinkSync(envPath);
|
|
14522
14448
|
} else {
|
|
@@ -14546,7 +14472,7 @@ function findMainCss2(cwd) {
|
|
|
14546
14472
|
"globals.css"
|
|
14547
14473
|
];
|
|
14548
14474
|
for (const candidate of candidates) {
|
|
14549
|
-
const filePath =
|
|
14475
|
+
const filePath = path39.join(cwd, candidate);
|
|
14550
14476
|
if (fs35.existsSync(filePath)) return filePath;
|
|
14551
14477
|
}
|
|
14552
14478
|
return void 0;
|
|
@@ -14562,17 +14488,19 @@ function isCLICreatedBiome(biomePath) {
|
|
|
14562
14488
|
}
|
|
14563
14489
|
function buildUninstallPlan(cwd) {
|
|
14564
14490
|
const steps = [];
|
|
14565
|
-
const hasSrc = fs35.existsSync(
|
|
14491
|
+
const hasSrc = fs35.existsSync(path39.join(cwd, "src"));
|
|
14566
14492
|
const appBase = hasSrc ? "src/app" : "app";
|
|
14567
14493
|
const dirs = [];
|
|
14568
|
-
const cmsDir =
|
|
14569
|
-
const cmsRouteGroup =
|
|
14494
|
+
const cmsDir = path39.join(cwd, "cms");
|
|
14495
|
+
const cmsRouteGroup = path39.join(cwd, appBase, "(cms)");
|
|
14570
14496
|
if (fs35.existsSync(cmsDir)) dirs.push("cms/");
|
|
14571
14497
|
if (fs35.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
|
|
14572
14498
|
if (dirs.length > 0) {
|
|
14573
14499
|
steps.push({
|
|
14574
14500
|
label: "CMS directories",
|
|
14575
14501
|
items: dirs,
|
|
14502
|
+
count: dirs.length,
|
|
14503
|
+
unit: dirs.length === 1 ? "directory" : "directories",
|
|
14576
14504
|
execute() {
|
|
14577
14505
|
if (fs35.existsSync(cmsDir)) fs35.rmSync(cmsDir, { recursive: true, force: true });
|
|
14578
14506
|
if (fs35.existsSync(cmsRouteGroup)) fs35.rmSync(cmsRouteGroup, { recursive: true, force: true });
|
|
@@ -14582,9 +14510,9 @@ function buildUninstallPlan(cwd) {
|
|
|
14582
14510
|
const configFiles = [];
|
|
14583
14511
|
const configPaths = [];
|
|
14584
14512
|
const candidates = [
|
|
14585
|
-
["cms.config.ts",
|
|
14586
|
-
["drizzle.config.ts",
|
|
14587
|
-
["CMS.md",
|
|
14513
|
+
["cms.config.ts", path39.join(cwd, "cms.config.ts")],
|
|
14514
|
+
["drizzle.config.ts", path39.join(cwd, "drizzle.config.ts")],
|
|
14515
|
+
["CMS.md", path39.join(cwd, "CMS.md")]
|
|
14588
14516
|
];
|
|
14589
14517
|
for (const [label, fullPath] of candidates) {
|
|
14590
14518
|
if (fs35.existsSync(fullPath)) {
|
|
@@ -14592,7 +14520,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14592
14520
|
configPaths.push(fullPath);
|
|
14593
14521
|
}
|
|
14594
14522
|
}
|
|
14595
|
-
const biomePath =
|
|
14523
|
+
const biomePath = path39.join(cwd, "biome.json");
|
|
14596
14524
|
if (isCLICreatedBiome(biomePath)) {
|
|
14597
14525
|
configFiles.push("biome.json (CLI-created)");
|
|
14598
14526
|
configPaths.push(biomePath);
|
|
@@ -14601,6 +14529,8 @@ function buildUninstallPlan(cwd) {
|
|
|
14601
14529
|
steps.push({
|
|
14602
14530
|
label: "Config files",
|
|
14603
14531
|
items: configFiles,
|
|
14532
|
+
count: configFiles.length,
|
|
14533
|
+
unit: configFiles.length === 1 ? "file" : "files",
|
|
14604
14534
|
execute() {
|
|
14605
14535
|
for (const p6 of configPaths) {
|
|
14606
14536
|
if (fs35.existsSync(p6)) fs35.unlinkSync(p6);
|
|
@@ -14608,13 +14538,17 @@ function buildUninstallPlan(cwd) {
|
|
|
14608
14538
|
}
|
|
14609
14539
|
});
|
|
14610
14540
|
}
|
|
14611
|
-
const tsconfigPath =
|
|
14541
|
+
const tsconfigPath = path39.join(cwd, "tsconfig.json");
|
|
14612
14542
|
if (fs35.existsSync(tsconfigPath)) {
|
|
14613
14543
|
const content = fs35.readFileSync(tsconfigPath, "utf-8");
|
|
14614
|
-
|
|
14544
|
+
const aliasMatches = content.match(/"@cms\//g);
|
|
14545
|
+
if (aliasMatches && aliasMatches.length > 0) {
|
|
14546
|
+
const aliasCount = aliasMatches.length;
|
|
14615
14547
|
steps.push({
|
|
14616
14548
|
label: "tsconfig.json path aliases",
|
|
14617
|
-
items: [
|
|
14549
|
+
items: [`@cms/* aliases in tsconfig.json`],
|
|
14550
|
+
count: aliasCount,
|
|
14551
|
+
unit: aliasCount === 1 ? "alias" : "aliases",
|
|
14618
14552
|
execute() {
|
|
14619
14553
|
cleanTsconfig(tsconfigPath);
|
|
14620
14554
|
}
|
|
@@ -14624,26 +14558,30 @@ function buildUninstallPlan(cwd) {
|
|
|
14624
14558
|
const cssFile = findMainCss2(cwd);
|
|
14625
14559
|
if (cssFile) {
|
|
14626
14560
|
const cssContent = fs35.readFileSync(cssFile, "utf-8");
|
|
14627
|
-
const
|
|
14628
|
-
if (
|
|
14629
|
-
const relCss =
|
|
14561
|
+
const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
|
|
14562
|
+
if (sourceLines.length > 0) {
|
|
14563
|
+
const relCss = path39.relative(cwd, cssFile);
|
|
14630
14564
|
steps.push({
|
|
14631
14565
|
label: `CSS @source lines (${relCss})`,
|
|
14632
|
-
items: [
|
|
14566
|
+
items: [`@source lines in ${relCss}`],
|
|
14567
|
+
count: sourceLines.length,
|
|
14568
|
+
unit: sourceLines.length === 1 ? "line" : "lines",
|
|
14633
14569
|
execute() {
|
|
14634
14570
|
cleanCss(cssFile);
|
|
14635
14571
|
}
|
|
14636
14572
|
});
|
|
14637
14573
|
}
|
|
14638
14574
|
}
|
|
14639
|
-
const envPath =
|
|
14575
|
+
const envPath = path39.join(cwd, ".env.local");
|
|
14640
14576
|
if (fs35.existsSync(envPath)) {
|
|
14641
14577
|
const envContent = fs35.readFileSync(envPath, "utf-8");
|
|
14642
14578
|
const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
|
|
14643
14579
|
if (bsVars.length > 0) {
|
|
14644
14580
|
steps.push({
|
|
14645
14581
|
label: ".env.local variables",
|
|
14646
|
-
items:
|
|
14582
|
+
items: ["BETTERSTART_* vars in .env.local"],
|
|
14583
|
+
count: bsVars.length,
|
|
14584
|
+
unit: bsVars.length === 1 ? "variable" : "variables",
|
|
14647
14585
|
execute() {
|
|
14648
14586
|
cleanEnvFile(envPath);
|
|
14649
14587
|
}
|
|
@@ -14652,26 +14590,24 @@ function buildUninstallPlan(cwd) {
|
|
|
14652
14590
|
}
|
|
14653
14591
|
return steps;
|
|
14654
14592
|
}
|
|
14655
|
-
var uninstallCommand = new
|
|
14656
|
-
const cwd = options.cwd ?
|
|
14593
|
+
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) => {
|
|
14594
|
+
const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
|
|
14657
14595
|
p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
|
|
14658
14596
|
const steps = buildUninstallPlan(cwd);
|
|
14659
14597
|
if (steps.length === 0) {
|
|
14660
|
-
p5.log.
|
|
14598
|
+
p5.log.success(`${pc3.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
|
|
14661
14599
|
p5.outro("Done");
|
|
14662
14600
|
return;
|
|
14663
14601
|
}
|
|
14664
|
-
|
|
14665
|
-
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
p5.log.message("");
|
|
14671
|
-
}
|
|
14602
|
+
const planLines = steps.map((step) => {
|
|
14603
|
+
const names = step.items.join(" ");
|
|
14604
|
+
const countLabel = pc3.dim(`${step.count} ${step.unit}`);
|
|
14605
|
+
return `${pc3.red("\xD7")} ${names} ${countLabel}`;
|
|
14606
|
+
});
|
|
14607
|
+
p5.note(planLines.join("\n"), "Uninstall plan");
|
|
14672
14608
|
if (!options.force) {
|
|
14673
14609
|
const confirmed = await p5.confirm({
|
|
14674
|
-
message:
|
|
14610
|
+
message: "Proceed with uninstall?",
|
|
14675
14611
|
initialValue: false
|
|
14676
14612
|
});
|
|
14677
14613
|
if (p5.isCancel(confirmed) || !confirmed) {
|
|
@@ -14679,29 +14615,45 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
|
|
|
14679
14615
|
process.exit(0);
|
|
14680
14616
|
}
|
|
14681
14617
|
}
|
|
14682
|
-
|
|
14618
|
+
const s = p5.spinner();
|
|
14619
|
+
s.start(steps[0].label);
|
|
14683
14620
|
for (const step of steps) {
|
|
14621
|
+
s.message(step.label);
|
|
14684
14622
|
step.execute();
|
|
14685
|
-
completedCount++;
|
|
14686
|
-
p5.log.success(`Removed: ${step.label}`);
|
|
14687
14623
|
}
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14624
|
+
const parts = steps.map((step) => `${step.count} ${step.unit}`);
|
|
14625
|
+
s.stop(`Removed ${parts.join(", ")}`);
|
|
14626
|
+
p5.note(pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
|
|
14627
|
+
p5.outro("Uninstall complete");
|
|
14628
|
+
});
|
|
14629
|
+
|
|
14630
|
+
// src/commands/update-deps.ts
|
|
14631
|
+
import path40 from "path";
|
|
14632
|
+
import * as clack2 from "@clack/prompts";
|
|
14633
|
+
import { Command as Command6 } from "commander";
|
|
14634
|
+
var updateDepsCommand = new Command6("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
|
|
14635
|
+
const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
|
|
14636
|
+
clack2.intro("BetterStart Update Dependencies");
|
|
14637
|
+
const pm = detectPackageManager(cwd);
|
|
14638
|
+
clack2.log.info(`Package manager: ${pm}`);
|
|
14639
|
+
const config = await resolveConfig(cwd);
|
|
14640
|
+
const includeEmail = config.features?.email ?? true;
|
|
14641
|
+
const s = clack2.spinner();
|
|
14642
|
+
s.start("Installing dependencies...");
|
|
14643
|
+
const result = await installDependenciesAsync({
|
|
14644
|
+
cwd,
|
|
14645
|
+
pm,
|
|
14646
|
+
includeEmail,
|
|
14647
|
+
includeBiome: false
|
|
14648
|
+
});
|
|
14649
|
+
if (result.success) {
|
|
14650
|
+
s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
|
|
14691
14651
|
} else {
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
);
|
|
14696
|
-
}
|
|
14697
|
-
if (findMainCss2(cwd)) {
|
|
14698
|
-
p5.log.info(
|
|
14699
|
-
pc3.dim(
|
|
14700
|
-
"Note: @theme tokens were left in your CSS \u2014 they're harmless and may be shared with your own styles."
|
|
14701
|
-
)
|
|
14702
|
-
);
|
|
14652
|
+
s.stop("Dependency install failed");
|
|
14653
|
+
clack2.log.error(result.error ?? "Unknown error");
|
|
14654
|
+
process.exit(1);
|
|
14703
14655
|
}
|
|
14704
|
-
|
|
14656
|
+
clack2.outro("Dependencies updated");
|
|
14705
14657
|
});
|
|
14706
14658
|
|
|
14707
14659
|
// src/commands/update-styles.ts
|