@betterstart/cli 0.1.28 → 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 +742 -869
- 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(group2);
|
|
6573
|
-
}
|
|
6574
|
-
if (!group2.children) {
|
|
6575
|
-
group2.children = [];
|
|
6576
|
-
}
|
|
6577
|
-
const existingChild = group2.children.findIndex((c) => c.href === entityHref);
|
|
6578
|
-
if (existingChild >= 0) {
|
|
6579
|
-
if (options.force) {
|
|
6580
|
-
group2.children[existingChild] = newItem;
|
|
6581
|
-
} else {
|
|
6582
|
-
return { files: [] };
|
|
6583
|
-
}
|
|
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
|
-
group2.children.sort((a, b) => a.label.localeCompare(b.label));
|
|
6587
|
-
}
|
|
6588
|
-
if (schema.navGroup.icon && !iconImports.includes(schema.navGroup.icon)) {
|
|
6589
|
-
iconImports.push(schema.navGroup.icon);
|
|
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
|
`;
|
|
@@ -8059,8 +7949,8 @@ export const { GET, POST } = toNextJsHandler(auth)
|
|
|
8059
7949
|
function uploadRouteTemplate() {
|
|
8060
7950
|
return `import { PutObjectCommand } from '@aws-sdk/client-s3'
|
|
8061
7951
|
import { BUCKET_NAME, generateFilePath, getPublicUrl, getR2Client } from '@cms/lib/r2'
|
|
8062
|
-
import { validateFiles } from '@cms/utils/validation'
|
|
8063
7952
|
import type { UploadedFile } from '@cms/types'
|
|
7953
|
+
import { validateFiles } from '@cms/utils/validation'
|
|
8064
7954
|
import { type NextRequest, NextResponse } from 'next/server'
|
|
8065
7955
|
|
|
8066
7956
|
export async function POST(request: NextRequest) {
|
|
@@ -8080,27 +7970,22 @@ export async function POST(request: NextRequest) {
|
|
|
8080
7970
|
}
|
|
8081
7971
|
|
|
8082
7972
|
if (files.length === 0) {
|
|
8083
|
-
return NextResponse.json(
|
|
8084
|
-
{ success: false, error: 'No files provided' },
|
|
8085
|
-
{ status: 400 }
|
|
8086
|
-
)
|
|
7973
|
+
return NextResponse.json({ success: false, error: 'No files provided' }, { status: 400 })
|
|
8087
7974
|
}
|
|
8088
7975
|
|
|
8089
7976
|
const validation = validateFiles(files, {
|
|
8090
7977
|
maxSizeInBytes,
|
|
8091
7978
|
allowedTypes,
|
|
8092
|
-
maxFiles: 10
|
|
7979
|
+
maxFiles: 10,
|
|
8093
7980
|
})
|
|
8094
7981
|
|
|
8095
7982
|
if (!validation.valid) {
|
|
8096
7983
|
return NextResponse.json(
|
|
8097
7984
|
{
|
|
8098
7985
|
success: false,
|
|
8099
|
-
error: validation.errors
|
|
8100
|
-
.map((e) => \`\${e.filename}: \${e.error}\`)
|
|
8101
|
-
.join('; ')
|
|
7986
|
+
error: validation.errors.map((e) => \`\${e.filename}: \${e.error}\`).join('; '),
|
|
8102
7987
|
},
|
|
8103
|
-
{ status: 400 }
|
|
7988
|
+
{ status: 400 },
|
|
8104
7989
|
)
|
|
8105
7990
|
}
|
|
8106
7991
|
|
|
@@ -8116,7 +8001,7 @@ export async function POST(request: NextRequest) {
|
|
|
8116
8001
|
Key: key,
|
|
8117
8002
|
Body: buffer,
|
|
8118
8003
|
ContentType: file.type,
|
|
8119
|
-
ContentLength: file.size
|
|
8004
|
+
ContentLength: file.size,
|
|
8120
8005
|
})
|
|
8121
8006
|
|
|
8122
8007
|
await getR2Client().send(command)
|
|
@@ -8126,18 +8011,14 @@ export async function POST(request: NextRequest) {
|
|
|
8126
8011
|
url: getPublicUrl(key),
|
|
8127
8012
|
filename: file.name,
|
|
8128
8013
|
size: file.size,
|
|
8129
|
-
contentType: file.type
|
|
8014
|
+
contentType: file.type,
|
|
8130
8015
|
})
|
|
8131
8016
|
}
|
|
8132
8017
|
|
|
8133
8018
|
return NextResponse.json({ success: true, files: uploadedFiles })
|
|
8134
8019
|
} catch (error) {
|
|
8135
|
-
const message =
|
|
8136
|
-
|
|
8137
|
-
return NextResponse.json(
|
|
8138
|
-
{ success: false, error: message },
|
|
8139
|
-
{ status: 500 }
|
|
8140
|
-
)
|
|
8020
|
+
const message = error instanceof Error ? error.message : 'Failed to upload files'
|
|
8021
|
+
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
|
8141
8022
|
}
|
|
8142
8023
|
}
|
|
8143
8024
|
`;
|
|
@@ -8217,7 +8098,9 @@ function authClientTemplate() {
|
|
|
8217
8098
|
import { createAuthClient } from 'better-auth/react'
|
|
8218
8099
|
|
|
8219
8100
|
export const authClient = createAuthClient({
|
|
8220
|
-
baseURL:
|
|
8101
|
+
baseURL:
|
|
8102
|
+
process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
|
|
8103
|
+
(typeof window !== 'undefined' ? window.location.origin : ''),
|
|
8221
8104
|
basePath: '/api/cms/auth',
|
|
8222
8105
|
})
|
|
8223
8106
|
|
|
@@ -8238,10 +8121,7 @@ export enum UserRole {
|
|
|
8238
8121
|
}
|
|
8239
8122
|
|
|
8240
8123
|
export function isUserRole(value: unknown): value is UserRole {
|
|
8241
|
-
return (
|
|
8242
|
-
typeof value === 'string' &&
|
|
8243
|
-
Object.values(UserRole).includes(value as UserRole)
|
|
8244
|
-
)
|
|
8124
|
+
return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
|
|
8245
8125
|
}
|
|
8246
8126
|
|
|
8247
8127
|
/**
|
|
@@ -8696,184 +8576,121 @@ function hasEnvBetterstartVars(cwd) {
|
|
|
8696
8576
|
// src/init/templates/components/cms-globals.ts
|
|
8697
8577
|
function cmsGlobalsCssTemplate() {
|
|
8698
8578
|
return `@import "tailwindcss";
|
|
8579
|
+
@import "tw-animate-css";
|
|
8580
|
+
@import "shadcn/tailwind.css";
|
|
8699
8581
|
|
|
8700
8582
|
@custom-variant dark (&:is(.dark *));
|
|
8701
8583
|
|
|
8702
|
-
:root {
|
|
8703
|
-
--background: oklch(0.985 0 0);
|
|
8704
|
-
--foreground: oklch(0 0 0);
|
|
8705
|
-
--card: oklch(1 0 0);
|
|
8706
|
-
--card-foreground: oklch(0 0 0);
|
|
8707
|
-
--popover: oklch(0.99 0 0);
|
|
8708
|
-
--popover-foreground: oklch(0 0 0);
|
|
8709
|
-
--primary: oklch(0 0 0);
|
|
8710
|
-
--primary-foreground: oklch(1 0 0);
|
|
8711
|
-
--secondary: oklch(0.97 0 0);
|
|
8712
|
-
--secondary-foreground: oklch(0 0 0);
|
|
8713
|
-
--muted: oklch(0.97 0 0);
|
|
8714
|
-
--muted-foreground: oklch(0.44 0 0);
|
|
8715
|
-
--accent: oklch(0.97 0 0);
|
|
8716
|
-
--accent-foreground: oklch(0 0 0);
|
|
8717
|
-
--destructive: oklch(0.63 0.19 23.03);
|
|
8718
|
-
--destructive-foreground: oklch(1 0 0);
|
|
8719
|
-
--border: oklch(0.92 0 0);
|
|
8720
|
-
--input: oklch(0.99 0 0);
|
|
8721
|
-
--ring: oklch(0 0 0);
|
|
8722
|
-
--chart-1: oklch(0.81 0.17 75.35);
|
|
8723
|
-
--chart-2: oklch(0.55 0.22 264.53);
|
|
8724
|
-
--chart-3: oklch(0.72 0 0);
|
|
8725
|
-
--chart-4: oklch(0.92 0 0);
|
|
8726
|
-
--chart-5: oklch(0.56 0 0);
|
|
8727
|
-
--sidebar: oklch(1 0 0);
|
|
8728
|
-
--sidebar-foreground: oklch(0 0 0);
|
|
8729
|
-
--sidebar-primary: oklch(0 0 0);
|
|
8730
|
-
--sidebar-primary-foreground: oklch(1 0 0);
|
|
8731
|
-
--sidebar-accent: oklch(0.94 0 0);
|
|
8732
|
-
--sidebar-accent-foreground: oklch(0 0 0);
|
|
8733
|
-
--sidebar-border: oklch(0.94 0 0);
|
|
8734
|
-
--sidebar-ring: oklch(0 0 0);
|
|
8735
|
-
--font-sans: Geist, sans-serif;
|
|
8736
|
-
--font-serif: Georgia, serif;
|
|
8737
|
-
--font-mono: Geist Mono, monospace;
|
|
8738
|
-
--radius: 0.4rem;
|
|
8739
|
-
--shadow-x: 0px;
|
|
8740
|
-
--shadow-y: 1px;
|
|
8741
|
-
--shadow-blur: 3px;
|
|
8742
|
-
--shadow-spread: 0px;
|
|
8743
|
-
--shadow-opacity: 0.02;
|
|
8744
|
-
--shadow-color: hsl(0 0% 0%);
|
|
8745
|
-
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8746
|
-
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8747
|
-
--shadow-sm:
|
|
8748
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8749
|
-
--shadow:
|
|
8750
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8751
|
-
--shadow-md:
|
|
8752
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
|
|
8753
|
-
--shadow-lg:
|
|
8754
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
|
|
8755
|
-
--shadow-xl:
|
|
8756
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
|
|
8757
|
-
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
|
|
8758
|
-
--tracking-normal: 0em;
|
|
8759
|
-
--spacing: 0.25rem;
|
|
8760
|
-
}
|
|
8761
|
-
|
|
8762
|
-
.dark {
|
|
8763
|
-
--background: oklch(0 0 0);
|
|
8764
|
-
--foreground: oklch(1 0 0);
|
|
8765
|
-
--card: oklch(0.14 0 0);
|
|
8766
|
-
--card-foreground: oklch(1 0 0);
|
|
8767
|
-
--popover: oklch(0.18 0 0);
|
|
8768
|
-
--popover-foreground: oklch(1 0 0);
|
|
8769
|
-
--primary: oklch(1 0 0);
|
|
8770
|
-
--primary-foreground: oklch(0 0 0);
|
|
8771
|
-
--secondary: oklch(0.25 0 0);
|
|
8772
|
-
--secondary-foreground: oklch(1 0 0);
|
|
8773
|
-
--muted: oklch(0.23 0 0);
|
|
8774
|
-
--muted-foreground: oklch(0.72 0 0);
|
|
8775
|
-
--accent: oklch(0.32 0 0);
|
|
8776
|
-
--accent-foreground: oklch(1 0 0);
|
|
8777
|
-
--destructive: oklch(0.69 0.2 23.91);
|
|
8778
|
-
--destructive-foreground: oklch(0 0 0);
|
|
8779
|
-
--border: oklch(0.26 0 0);
|
|
8780
|
-
--input: oklch(0.32 0 0);
|
|
8781
|
-
--ring: oklch(0.72 0 0);
|
|
8782
|
-
--chart-1: oklch(0.81 0.17 75.35);
|
|
8783
|
-
--chart-2: oklch(0.58 0.21 260.84);
|
|
8784
|
-
--chart-3: oklch(0.56 0 0);
|
|
8785
|
-
--chart-4: oklch(0.44 0 0);
|
|
8786
|
-
--chart-5: oklch(0.92 0 0);
|
|
8787
|
-
--sidebar: oklch(0.18 0 0);
|
|
8788
|
-
--sidebar-foreground: oklch(1 0 0);
|
|
8789
|
-
--sidebar-primary: oklch(1 0 0);
|
|
8790
|
-
--sidebar-primary-foreground: oklch(0 0 0);
|
|
8791
|
-
--sidebar-accent: oklch(0.32 0 0);
|
|
8792
|
-
--sidebar-accent-foreground: oklch(1 0 0);
|
|
8793
|
-
--sidebar-border: oklch(0.32 0 0);
|
|
8794
|
-
--sidebar-ring: oklch(0.72 0 0);
|
|
8795
|
-
--font-sans: Geist, sans-serif;
|
|
8796
|
-
--font-serif: Georgia, serif;
|
|
8797
|
-
--font-mono: Geist Mono, monospace;
|
|
8798
|
-
--radius: 0.4rem;
|
|
8799
|
-
--shadow-x: 0px;
|
|
8800
|
-
--shadow-y: 1px;
|
|
8801
|
-
--shadow-blur: 3px;
|
|
8802
|
-
--shadow-spread: 0px;
|
|
8803
|
-
--shadow-opacity: 0.02;
|
|
8804
|
-
--shadow-color: hsl(0 0% 0%);
|
|
8805
|
-
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8806
|
-
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
|
|
8807
|
-
--shadow-sm:
|
|
8808
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8809
|
-
--shadow:
|
|
8810
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
|
|
8811
|
-
--shadow-md:
|
|
8812
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
|
|
8813
|
-
--shadow-lg:
|
|
8814
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
|
|
8815
|
-
--shadow-xl:
|
|
8816
|
-
0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
|
|
8817
|
-
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
|
|
8818
|
-
}
|
|
8819
|
-
|
|
8820
|
-
.cms-root {
|
|
8821
|
-
--font-sans: var(--font-geist-sans, sans-serif);
|
|
8822
|
-
--font-mono: var(--font-geist-mono, monospace);
|
|
8823
|
-
font-family: var(--font-sans);
|
|
8824
|
-
}
|
|
8825
|
-
|
|
8826
8584
|
@theme inline {
|
|
8827
8585
|
--color-background: var(--background);
|
|
8828
8586
|
--color-foreground: var(--foreground);
|
|
8829
|
-
--color-card: var(--card);
|
|
8830
|
-
--color-card-foreground: var(--card-foreground);
|
|
8831
|
-
--color-popover: var(--popover);
|
|
8832
|
-
--color-popover-foreground: var(--popover-foreground);
|
|
8833
|
-
--color-primary: var(--primary);
|
|
8834
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
8835
|
-
--color-secondary: var(--secondary);
|
|
8836
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
8837
|
-
--color-muted: var(--muted);
|
|
8838
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
8839
|
-
--color-accent: var(--accent);
|
|
8840
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
8841
|
-
--color-destructive: var(--destructive);
|
|
8842
|
-
--color-destructive-foreground: var(--destructive-foreground);
|
|
8843
|
-
--color-border: var(--border);
|
|
8844
|
-
--color-input: var(--input);
|
|
8845
|
-
--color-ring: var(--ring);
|
|
8846
|
-
--color-chart-1: var(--chart-1);
|
|
8847
|
-
--color-chart-2: var(--chart-2);
|
|
8848
|
-
--color-chart-3: var(--chart-3);
|
|
8849
|
-
--color-chart-4: var(--chart-4);
|
|
8850
|
-
--color-chart-5: var(--chart-5);
|
|
8851
|
-
--color-sidebar: var(--sidebar);
|
|
8852
|
-
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
8853
|
-
--color-sidebar-primary: var(--sidebar-primary);
|
|
8854
|
-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
8855
|
-
--color-sidebar-accent: var(--sidebar-accent);
|
|
8856
|
-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
8857
|
-
--color-sidebar-border: var(--sidebar-border);
|
|
8858
|
-
--color-sidebar-ring: var(--sidebar-ring);
|
|
8859
|
-
|
|
8860
8587
|
--font-sans: var(--font-sans);
|
|
8861
|
-
--font-mono: var(--font-mono);
|
|
8862
|
-
--
|
|
8863
|
-
|
|
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);
|
|
8864
8618
|
--radius-sm: calc(var(--radius) - 4px);
|
|
8865
8619
|
--radius-md: calc(var(--radius) - 2px);
|
|
8866
8620
|
--radius-lg: var(--radius);
|
|
8867
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
|
+
}
|
|
8868
8661
|
|
|
8869
|
-
|
|
8870
|
-
--
|
|
8871
|
-
--
|
|
8872
|
-
--
|
|
8873
|
-
--
|
|
8874
|
-
--
|
|
8875
|
-
--
|
|
8876
|
-
--
|
|
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);
|
|
8877
8694
|
}
|
|
8878
8695
|
|
|
8879
8696
|
@layer base {
|
|
@@ -8881,7 +8698,7 @@ function cmsGlobalsCssTemplate() {
|
|
|
8881
8698
|
@apply border-border outline-ring/50;
|
|
8882
8699
|
}
|
|
8883
8700
|
body {
|
|
8884
|
-
@apply bg-background text-foreground
|
|
8701
|
+
@apply bg-background text-foreground;
|
|
8885
8702
|
}
|
|
8886
8703
|
}
|
|
8887
8704
|
|
|
@@ -8914,6 +8731,14 @@ function cmsGlobalsCssTemplate() {
|
|
|
8914
8731
|
function dataTableTemplate() {
|
|
8915
8732
|
return `'use client'
|
|
8916
8733
|
|
|
8734
|
+
import {
|
|
8735
|
+
Table,
|
|
8736
|
+
TableBody,
|
|
8737
|
+
TableCell,
|
|
8738
|
+
TableHead,
|
|
8739
|
+
TableHeader,
|
|
8740
|
+
TableRow,
|
|
8741
|
+
} from '@cms/components/ui/table'
|
|
8917
8742
|
import {
|
|
8918
8743
|
type ColumnDef,
|
|
8919
8744
|
type ColumnFiltersState,
|
|
@@ -8923,20 +8748,11 @@ import {
|
|
|
8923
8748
|
getPaginationRowModel,
|
|
8924
8749
|
getSortedRowModel,
|
|
8925
8750
|
type SortingState,
|
|
8926
|
-
type Table as TanstackTable,
|
|
8927
8751
|
useReactTable,
|
|
8928
|
-
type VisibilityState
|
|
8752
|
+
type VisibilityState,
|
|
8929
8753
|
} from '@tanstack/react-table'
|
|
8930
8754
|
import { parseAsInteger, useQueryState } from 'nuqs'
|
|
8931
8755
|
import * as React from 'react'
|
|
8932
|
-
import {
|
|
8933
|
-
Table,
|
|
8934
|
-
TableBody,
|
|
8935
|
-
TableCell,
|
|
8936
|
-
TableHead,
|
|
8937
|
-
TableHeader,
|
|
8938
|
-
TableRow
|
|
8939
|
-
} from '@cms/components/ui/table'
|
|
8940
8756
|
import { DataTablePagination } from './data-table-pagination'
|
|
8941
8757
|
|
|
8942
8758
|
interface DataTableProps<TData, TValue> {
|
|
@@ -8962,7 +8778,7 @@ export function DataTable<TData, TValue>({
|
|
|
8962
8778
|
selectedIds,
|
|
8963
8779
|
onSelectedIdsChange,
|
|
8964
8780
|
meta,
|
|
8965
|
-
getId = (row) => (row as { id: number }).id
|
|
8781
|
+
getId = (row) => (row as { id: number }).id,
|
|
8966
8782
|
}: DataTableProps<TData, TValue>) {
|
|
8967
8783
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
8968
8784
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
|
@@ -8985,27 +8801,38 @@ export function DataTable<TData, TValue>({
|
|
|
8985
8801
|
}, [selectedIds, data, getId])
|
|
8986
8802
|
|
|
8987
8803
|
const handleRowSelectionChange = React.useCallback(
|
|
8988
|
-
(
|
|
8804
|
+
(
|
|
8805
|
+
updater:
|
|
8806
|
+
| Record<string, boolean>
|
|
8807
|
+
| ((old: Record<string, boolean>) => Record<string, boolean>),
|
|
8808
|
+
) => {
|
|
8989
8809
|
if (!onSelectedIdsChange) return
|
|
8990
8810
|
const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater
|
|
8991
8811
|
const newIds = Object.keys(newSelection)
|
|
8992
8812
|
.filter((key) => newSelection[key])
|
|
8993
|
-
.map((key) => getId(data[Number.parseInt(key)]))
|
|
8813
|
+
.map((key) => getId(data[Number.parseInt(key, 10)]))
|
|
8994
8814
|
.filter(Boolean)
|
|
8995
8815
|
onSelectedIdsChange(newIds)
|
|
8996
8816
|
},
|
|
8997
|
-
[data, rowSelection, onSelectedIdsChange, getId]
|
|
8817
|
+
[data, rowSelection, onSelectedIdsChange, getId],
|
|
8998
8818
|
)
|
|
8999
8819
|
|
|
9000
8820
|
const handlePaginationChange = React.useCallback(
|
|
9001
|
-
(
|
|
8821
|
+
(
|
|
8822
|
+
updater:
|
|
8823
|
+
| { pageIndex: number; pageSize: number }
|
|
8824
|
+
| ((old: { pageIndex: number; pageSize: number }) => {
|
|
8825
|
+
pageIndex: number
|
|
8826
|
+
pageSize: number
|
|
8827
|
+
}),
|
|
8828
|
+
) => {
|
|
9002
8829
|
const current = { pageIndex, pageSize: effectivePageSize }
|
|
9003
8830
|
const next = typeof updater === 'function' ? updater(current) : updater
|
|
9004
8831
|
React.startTransition(() => {
|
|
9005
8832
|
setPageIndex(next.pageIndex)
|
|
9006
8833
|
})
|
|
9007
8834
|
},
|
|
9008
|
-
[pageIndex, effectivePageSize, setPageIndex]
|
|
8835
|
+
[pageIndex, effectivePageSize, setPageIndex],
|
|
9009
8836
|
)
|
|
9010
8837
|
|
|
9011
8838
|
const table = useReactTable({
|
|
@@ -9028,9 +8855,9 @@ export function DataTable<TData, TValue>({
|
|
|
9028
8855
|
rowSelection,
|
|
9029
8856
|
pagination: {
|
|
9030
8857
|
pageIndex,
|
|
9031
|
-
pageSize: effectivePageSize
|
|
9032
|
-
}
|
|
9033
|
-
}
|
|
8858
|
+
pageSize: effectivePageSize,
|
|
8859
|
+
},
|
|
8860
|
+
},
|
|
9034
8861
|
})
|
|
9035
8862
|
|
|
9036
8863
|
return (
|
|
@@ -9097,23 +8924,23 @@ export type { DataTableProps }
|
|
|
9097
8924
|
function dataTablePaginationTemplate() {
|
|
9098
8925
|
return `'use client'
|
|
9099
8926
|
|
|
9100
|
-
import type { Table } from '@tanstack/react-table'
|
|
9101
|
-
import * as React from 'react'
|
|
9102
8927
|
import { Button } from '@cms/components/ui/button'
|
|
9103
8928
|
import {
|
|
9104
8929
|
Select,
|
|
9105
8930
|
SelectContent,
|
|
9106
8931
|
SelectItem,
|
|
9107
8932
|
SelectTrigger,
|
|
9108
|
-
SelectValue
|
|
8933
|
+
SelectValue,
|
|
9109
8934
|
} from '@cms/components/ui/select'
|
|
8935
|
+
import type { Table } from '@tanstack/react-table'
|
|
8936
|
+
import * as React from 'react'
|
|
9110
8937
|
|
|
9111
8938
|
const PAGE_SIZE_OPTIONS = [
|
|
9112
8939
|
{ value: '10', label: '10' },
|
|
9113
8940
|
{ value: '20', label: '20' },
|
|
9114
8941
|
{ value: '50', label: '50' },
|
|
9115
8942
|
{ value: '100', label: '100' },
|
|
9116
|
-
{ value: 'all', label: 'All' }
|
|
8943
|
+
{ value: 'all', label: 'All' },
|
|
9117
8944
|
]
|
|
9118
8945
|
|
|
9119
8946
|
interface DataTablePaginationProps<TData> {
|
|
@@ -9125,7 +8952,7 @@ interface DataTablePaginationProps<TData> {
|
|
|
9125
8952
|
export function DataTablePagination<TData>({
|
|
9126
8953
|
table,
|
|
9127
8954
|
pageSize,
|
|
9128
|
-
setPageSize
|
|
8955
|
+
setPageSize,
|
|
9129
8956
|
}: DataTablePaginationProps<TData>) {
|
|
9130
8957
|
const handlePageSizeChange = React.useCallback(
|
|
9131
8958
|
(value: string) => {
|
|
@@ -9138,7 +8965,7 @@ export function DataTablePagination<TData>({
|
|
|
9138
8965
|
table.setPageIndex(0)
|
|
9139
8966
|
})
|
|
9140
8967
|
},
|
|
9141
|
-
[setPageSize, table]
|
|
8968
|
+
[setPageSize, table],
|
|
9142
8969
|
)
|
|
9143
8970
|
|
|
9144
8971
|
return (
|
|
@@ -9192,11 +9019,11 @@ export function DataTablePagination<TData>({
|
|
|
9192
9019
|
function dataTableToolbarTemplate() {
|
|
9193
9020
|
return `'use client'
|
|
9194
9021
|
|
|
9195
|
-
import { ArrowUpDown, Save, Search } from 'lucide-react'
|
|
9196
|
-
import * as React from 'react'
|
|
9197
|
-
import { useFormStatus } from 'react-dom'
|
|
9198
9022
|
import { Button } from '@cms/components/ui/button'
|
|
9199
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'
|
|
9200
9027
|
|
|
9201
9028
|
function SearchButton() {
|
|
9202
9029
|
const { pending } = useFormStatus()
|
|
@@ -9218,7 +9045,7 @@ export function DataTableToolbar({
|
|
|
9218
9045
|
search,
|
|
9219
9046
|
onSearch,
|
|
9220
9047
|
searchPlaceholder = 'Search...',
|
|
9221
|
-
children
|
|
9048
|
+
children,
|
|
9222
9049
|
}: DataTableToolbarProps) {
|
|
9223
9050
|
return (
|
|
9224
9051
|
<div className="flex items-center gap-2">
|
|
@@ -9255,7 +9082,7 @@ export function ReorderControls({
|
|
|
9255
9082
|
onSave,
|
|
9256
9083
|
onCancel,
|
|
9257
9084
|
hasChanges,
|
|
9258
|
-
isSaving
|
|
9085
|
+
isSaving,
|
|
9259
9086
|
}: ReorderControlsProps) {
|
|
9260
9087
|
return (
|
|
9261
9088
|
<div className="flex items-center gap-2">
|
|
@@ -9270,26 +9097,14 @@ export function ReorderControls({
|
|
|
9270
9097
|
</Button>
|
|
9271
9098
|
{reorderMode && (
|
|
9272
9099
|
<>
|
|
9273
|
-
<Button
|
|
9274
|
-
variant="default"
|
|
9275
|
-
size="sm"
|
|
9276
|
-
onClick={onSave}
|
|
9277
|
-
disabled={!hasChanges || isSaving}
|
|
9278
|
-
>
|
|
9100
|
+
<Button variant="default" size="sm" onClick={onSave} disabled={!hasChanges || isSaving}>
|
|
9279
9101
|
<Save className="size-4 mr-1" />
|
|
9280
9102
|
{isSaving ? 'Saving...' : 'Save'}
|
|
9281
9103
|
</Button>
|
|
9282
|
-
<Button
|
|
9283
|
-
variant="outline"
|
|
9284
|
-
size="sm"
|
|
9285
|
-
onClick={onCancel}
|
|
9286
|
-
disabled={isSaving}
|
|
9287
|
-
>
|
|
9104
|
+
<Button variant="outline" size="sm" onClick={onCancel} disabled={isSaving}>
|
|
9288
9105
|
Cancel
|
|
9289
9106
|
</Button>
|
|
9290
|
-
{hasChanges &&
|
|
9291
|
-
<span className="text-sm text-muted-foreground">Unsaved changes</span>
|
|
9292
|
-
)}
|
|
9107
|
+
{hasChanges && <span className="text-sm text-muted-foreground">Unsaved changes</span>}
|
|
9293
9108
|
</>
|
|
9294
9109
|
)}
|
|
9295
9110
|
</div>
|
|
@@ -9302,7 +9117,6 @@ export function ReorderControls({
|
|
|
9302
9117
|
function cmsHeaderTemplate() {
|
|
9303
9118
|
return `'use client'
|
|
9304
9119
|
|
|
9305
|
-
import { CmsSearch } from '@cms/components/layout/cms-search'
|
|
9306
9120
|
import { Button } from '@cms/components/ui/button'
|
|
9307
9121
|
import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
|
|
9308
9122
|
import { useTheme } from '@cms/hooks/use-cms-theme'
|
|
@@ -9313,11 +9127,10 @@ export function CmsHeader() {
|
|
|
9313
9127
|
const { state } = useSidebar()
|
|
9314
9128
|
|
|
9315
9129
|
return (
|
|
9316
|
-
<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">
|
|
9317
9131
|
<div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
|
|
9318
9132
|
<div className="flex items-center gap-2 w-full">
|
|
9319
9133
|
{state === 'collapsed' && <SidebarTrigger />}
|
|
9320
|
-
<CmsSearch />
|
|
9321
9134
|
</div>
|
|
9322
9135
|
<div className="flex items-center gap-2 ml-auto">
|
|
9323
9136
|
<Button
|
|
@@ -9337,19 +9150,49 @@ export function CmsHeader() {
|
|
|
9337
9150
|
`;
|
|
9338
9151
|
}
|
|
9339
9152
|
|
|
9340
|
-
// src/init/templates/components/layout/cms-
|
|
9341
|
-
function
|
|
9153
|
+
// src/init/templates/components/layout/cms-nav-link.ts
|
|
9154
|
+
function cmsNavLinkTemplate() {
|
|
9342
9155
|
return `'use client'
|
|
9343
9156
|
|
|
9344
|
-
import {
|
|
9345
|
-
import
|
|
9346
|
-
import {
|
|
9347
|
-
import { Toaster } from '@cms/components/ui/sonner'
|
|
9348
|
-
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'
|
|
9349
9160
|
|
|
9350
|
-
export function
|
|
9351
|
-
|
|
9352
|
-
|
|
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 }) {
|
|
9194
|
+
const [queryClient] = useState(
|
|
9195
|
+
() =>
|
|
9353
9196
|
new QueryClient({
|
|
9354
9197
|
defaultOptions: {
|
|
9355
9198
|
queries: {
|
|
@@ -9385,15 +9228,15 @@ export const CmsSearch = () => {
|
|
|
9385
9228
|
<div className="flex items-center gap-2 relative w-full max-w-[240px]">
|
|
9386
9229
|
<Button
|
|
9387
9230
|
variant="outline"
|
|
9388
|
-
className="w-full text-left items-center pr-1! rounded-
|
|
9389
|
-
size="
|
|
9231
|
+
className="w-full text-left items-center pr-1.5! py-0 rounded-lg bg-white"
|
|
9232
|
+
size="lg"
|
|
9390
9233
|
>
|
|
9391
|
-
<Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground"
|
|
9392
|
-
<span className="w-full font-
|
|
9393
|
-
|
|
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...
|
|
9394
9237
|
</span>
|
|
9395
9238
|
<div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
|
|
9396
|
-
<Command className="size-3! text-muted-foreground"
|
|
9239
|
+
<Command className="size-3! text-muted-foreground" />
|
|
9397
9240
|
<span className="font-mono text-xs font-medium">K</span>
|
|
9398
9241
|
</div>
|
|
9399
9242
|
</Button>
|
|
@@ -9407,123 +9250,110 @@ CmsSearch.displayName = 'CmsSearch'
|
|
|
9407
9250
|
|
|
9408
9251
|
// src/init/templates/components/layout/cms-sidebar.ts
|
|
9409
9252
|
function cmsSidebarTemplate() {
|
|
9410
|
-
return `import {
|
|
9411
|
-
import { getSession } from '@cms/auth/middleware'
|
|
9253
|
+
return `import { getSession } from '@cms/auth/middleware'
|
|
9412
9254
|
import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
|
|
9413
|
-
import {
|
|
9414
|
-
Collapsible,
|
|
9415
|
-
CollapsibleContent,
|
|
9416
|
-
CollapsibleTrigger,
|
|
9417
|
-
} from '@cms/components/ui/collapsible'
|
|
9418
9255
|
import {
|
|
9419
9256
|
Sidebar,
|
|
9420
9257
|
SidebarContent,
|
|
9421
9258
|
SidebarFooter,
|
|
9259
|
+
SidebarGroup,
|
|
9260
|
+
SidebarGroupLabel,
|
|
9422
9261
|
SidebarHeader,
|
|
9423
9262
|
SidebarMenu,
|
|
9424
|
-
SidebarMenuButton,
|
|
9425
9263
|
SidebarMenuItem,
|
|
9426
|
-
SidebarMenuSub,
|
|
9427
|
-
SidebarMenuSubButton,
|
|
9428
|
-
SidebarMenuSubItem,
|
|
9429
|
-
SidebarRail,
|
|
9430
|
-
SidebarTrigger,
|
|
9431
9264
|
} from '@cms/components/ui/sidebar'
|
|
9432
9265
|
import { cms } from '@cms/data/cms'
|
|
9433
9266
|
import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
|
|
9434
|
-
import {
|
|
9267
|
+
import { Settings, Users } from 'lucide-react'
|
|
9435
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'
|
|
9436
9273
|
|
|
9437
|
-
function
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
<CollapsibleContent>
|
|
9450
|
-
<SidebarMenuSub>
|
|
9451
|
-
{item.children.map((child) => (
|
|
9452
|
-
<SidebarMenuSubItem key={child.href}>
|
|
9453
|
-
<SidebarMenuSubButton asChild>
|
|
9454
|
-
<Link href={child.href}>
|
|
9455
|
-
{child.icon && <child.icon className="size-3.5!" />}
|
|
9456
|
-
<span>{child.label}</span>
|
|
9457
|
-
</Link>
|
|
9458
|
-
</SidebarMenuSubButton>
|
|
9459
|
-
</SidebarMenuSubItem>
|
|
9460
|
-
))}
|
|
9461
|
-
</SidebarMenuSub>
|
|
9462
|
-
</CollapsibleContent>
|
|
9463
|
-
</SidebarMenuItem>
|
|
9464
|
-
</Collapsible>
|
|
9465
|
-
)
|
|
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)
|
|
9466
9286
|
}
|
|
9467
9287
|
|
|
9468
|
-
return
|
|
9469
|
-
<SidebarMenuItem className="px-2">
|
|
9470
|
-
<SidebarMenuButton asChild>
|
|
9471
|
-
<Link href={item.href}>
|
|
9472
|
-
{item.icon && <item.icon className="size-3.5!" />}
|
|
9473
|
-
<span>{item.label}</span>
|
|
9474
|
-
</Link>
|
|
9475
|
-
</SidebarMenuButton>
|
|
9476
|
-
</SidebarMenuItem>
|
|
9477
|
-
)
|
|
9288
|
+
return groups
|
|
9478
9289
|
}
|
|
9479
9290
|
|
|
9480
9291
|
export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
9481
9292
|
const session = await getSession()
|
|
9482
9293
|
const settings = await getSetting()
|
|
9483
9294
|
const user = session?.user ?? null
|
|
9295
|
+
const groups = groupNavItems(cmsNavigation)
|
|
9484
9296
|
|
|
9485
9297
|
return (
|
|
9486
9298
|
<Sidebar collapsible="icon" {...props}>
|
|
9487
9299
|
<SidebarHeader className="border-b border-border h-14 items-center flex w-full">
|
|
9488
9300
|
<div className="flex items-center gap-2 w-full relative h-full">
|
|
9489
9301
|
<Link href="/cms" className="flex items-center gap-2 w-full">
|
|
9490
|
-
<Avatar className="size-
|
|
9302
|
+
<Avatar className="size-8">
|
|
9491
9303
|
<AvatarImage src={'/favicon.ico'} />
|
|
9492
|
-
<AvatarFallback className="text-sm font-semibold">
|
|
9304
|
+
<AvatarFallback className="text-sm font-semibold text-foreground">
|
|
9493
9305
|
{settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
|
|
9494
9306
|
</AvatarFallback>
|
|
9495
9307
|
</Avatar>
|
|
9496
|
-
<div className="flex items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
|
|
9497
|
-
<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>
|
|
9498
9312
|
</div>
|
|
9499
9313
|
</Link>
|
|
9500
|
-
<SidebarTrigger className="hidden md:flex" />
|
|
9501
9314
|
</div>
|
|
9502
9315
|
</SidebarHeader>
|
|
9503
|
-
<SidebarContent
|
|
9504
|
-
<
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
<
|
|
9513
|
-
|
|
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 />
|
|
9514
9346
|
<span>Users</span>
|
|
9515
|
-
</
|
|
9516
|
-
</
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
<Link href="/cms/settings">
|
|
9521
|
-
<Settings className="size-3.5!" />
|
|
9347
|
+
</CmsNavLink>
|
|
9348
|
+
</SidebarMenuItem>
|
|
9349
|
+
<SidebarMenuItem>
|
|
9350
|
+
<CmsNavLink href="/cms/settings">
|
|
9351
|
+
<Settings />
|
|
9522
9352
|
<span>Settings</span>
|
|
9523
|
-
</
|
|
9524
|
-
</
|
|
9525
|
-
</
|
|
9526
|
-
</
|
|
9353
|
+
</CmsNavLink>
|
|
9354
|
+
</SidebarMenuItem>
|
|
9355
|
+
</SidebarMenu>
|
|
9356
|
+
</SidebarGroup>
|
|
9527
9357
|
</SidebarContent>
|
|
9528
9358
|
<SidebarFooter>
|
|
9529
9359
|
{user && (
|
|
@@ -9535,7 +9365,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
|
|
9535
9365
|
</div>
|
|
9536
9366
|
)}
|
|
9537
9367
|
</SidebarFooter>
|
|
9538
|
-
<SidebarRail />
|
|
9539
9368
|
</Sidebar>
|
|
9540
9369
|
)
|
|
9541
9370
|
}
|
|
@@ -9555,7 +9384,7 @@ import {
|
|
|
9555
9384
|
AlertDialogFooter,
|
|
9556
9385
|
AlertDialogHeader,
|
|
9557
9386
|
AlertDialogTitle,
|
|
9558
|
-
AlertDialogTrigger
|
|
9387
|
+
AlertDialogTrigger,
|
|
9559
9388
|
} from '@cms/components/ui/alert-dialog'
|
|
9560
9389
|
import { Button } from '@cms/components/ui/button'
|
|
9561
9390
|
import { Trash2 } from 'lucide-react'
|
|
@@ -9577,7 +9406,7 @@ export function DeleteDialog({
|
|
|
9577
9406
|
isPending = false,
|
|
9578
9407
|
title = 'Are you sure?',
|
|
9579
9408
|
description = 'This action cannot be undone. This will permanently delete this item.',
|
|
9580
|
-
trigger
|
|
9409
|
+
trigger,
|
|
9581
9410
|
}: DeleteDialogProps) {
|
|
9582
9411
|
return (
|
|
9583
9412
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -9626,18 +9455,24 @@ export function DeleteButton({ onClick, label = 'item', count }: DeleteButtonPro
|
|
|
9626
9455
|
function pageHeaderTemplate() {
|
|
9627
9456
|
return `interface PageHeaderProps {
|
|
9628
9457
|
title: string
|
|
9629
|
-
description: string
|
|
9630
9458
|
children?: React.ReactNode
|
|
9459
|
+
search?: React.ReactNode
|
|
9460
|
+
actions?: React.ReactNode
|
|
9461
|
+
back?: React.ReactNode
|
|
9631
9462
|
}
|
|
9632
9463
|
|
|
9633
|
-
export function PageHeader({ title,
|
|
9464
|
+
export function PageHeader({ title, children, search, actions, back }: PageHeaderProps) {
|
|
9634
9465
|
return (
|
|
9635
|
-
<div className="
|
|
9636
|
-
<div className="flex
|
|
9637
|
-
|
|
9638
|
-
<
|
|
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}
|
|
9639
9475
|
</div>
|
|
9640
|
-
{children && <div className="flex items-center gap-2">{children}</div>}
|
|
9641
9476
|
</div>
|
|
9642
9477
|
)
|
|
9643
9478
|
}
|
|
@@ -9656,7 +9491,7 @@ const statusStyles: Record<StatusVariant, string> = {
|
|
|
9656
9491
|
success: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
|
|
9657
9492
|
warning: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
|
|
9658
9493
|
error: 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300',
|
|
9659
|
-
info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300'
|
|
9494
|
+
info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
|
|
9660
9495
|
}
|
|
9661
9496
|
|
|
9662
9497
|
interface StatusBadgeProps extends Omit<BadgeProps, 'variant'> {
|
|
@@ -9674,7 +9509,11 @@ export function StatusBadge({ status, className, ...props }: StatusBadgeProps) {
|
|
|
9674
9509
|
}
|
|
9675
9510
|
|
|
9676
9511
|
/** Map boolean values to status badges */
|
|
9677
|
-
export function BooleanBadge({
|
|
9512
|
+
export function BooleanBadge({
|
|
9513
|
+
value,
|
|
9514
|
+
trueLabel = 'Yes',
|
|
9515
|
+
falseLabel = 'No',
|
|
9516
|
+
}: {
|
|
9678
9517
|
value: boolean
|
|
9679
9518
|
trueLabel?: string
|
|
9680
9519
|
falseLabel?: string
|
|
@@ -9698,22 +9537,44 @@ function cmsDataTemplate(projectName) {
|
|
|
9698
9537
|
|
|
9699
9538
|
// src/init/templates/data/navigation.ts
|
|
9700
9539
|
function navigationDataTemplate() {
|
|
9701
|
-
return `import {
|
|
9702
|
-
import
|
|
9540
|
+
return `import type { LucideIcon } from 'lucide-react'
|
|
9541
|
+
import { ChartSpline, FileText, House, ImagePlay, Tag } from 'lucide-react'
|
|
9703
9542
|
|
|
9704
9543
|
export interface CmsNavigationItem {
|
|
9705
9544
|
label: string
|
|
9706
9545
|
href: string
|
|
9707
9546
|
icon?: LucideIcon
|
|
9708
|
-
|
|
9547
|
+
group?: string
|
|
9709
9548
|
}
|
|
9710
9549
|
|
|
9711
9550
|
export const cmsNavigation: CmsNavigationItem[] = [
|
|
9712
9551
|
{
|
|
9713
|
-
label: '
|
|
9552
|
+
label: 'Overview',
|
|
9714
9553
|
href: '/cms',
|
|
9715
|
-
icon: House
|
|
9716
|
-
}
|
|
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
|
+
},
|
|
9717
9578
|
]
|
|
9718
9579
|
`;
|
|
9719
9580
|
}
|
|
@@ -9787,14 +9648,10 @@ export function CmsThemeProvider({ children }: { children: React.ReactNode }) {
|
|
|
9787
9648
|
|
|
9788
9649
|
const value = React.useMemo(
|
|
9789
9650
|
() => ({ theme, setTheme, resolvedTheme: resolved }),
|
|
9790
|
-
[theme, setTheme, resolved]
|
|
9651
|
+
[theme, setTheme, resolved],
|
|
9791
9652
|
)
|
|
9792
9653
|
|
|
9793
|
-
return
|
|
9794
|
-
<CmsThemeContext.Provider value={value}>
|
|
9795
|
-
{children}
|
|
9796
|
-
</CmsThemeContext.Provider>
|
|
9797
|
-
)
|
|
9654
|
+
return <CmsThemeContext.Provider value={value}>{children}</CmsThemeContext.Provider>
|
|
9798
9655
|
}
|
|
9799
9656
|
|
|
9800
9657
|
export function useTheme(): ThemeContext {
|
|
@@ -9834,11 +9691,11 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9834
9691
|
if (result.success && result.files) {
|
|
9835
9692
|
const images: EditorImageUploadResult[] = result.files.map((f) => ({
|
|
9836
9693
|
url: f.url,
|
|
9837
|
-
filename: f.filename
|
|
9694
|
+
filename: f.filename,
|
|
9838
9695
|
}))
|
|
9839
9696
|
onImagesUploadedRef.current(images)
|
|
9840
9697
|
}
|
|
9841
|
-
}
|
|
9698
|
+
},
|
|
9842
9699
|
})
|
|
9843
9700
|
|
|
9844
9701
|
const isUploading = mutation.isPending
|
|
@@ -9849,21 +9706,19 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9849
9706
|
if (imageFiles.length === 0) return
|
|
9850
9707
|
upload(imageFiles, 'images')
|
|
9851
9708
|
},
|
|
9852
|
-
[upload]
|
|
9709
|
+
[upload],
|
|
9853
9710
|
)
|
|
9854
9711
|
|
|
9855
9712
|
const handleDrop = React.useCallback(
|
|
9856
9713
|
(e: React.DragEvent) => {
|
|
9857
9714
|
e.preventDefault()
|
|
9858
9715
|
e.stopPropagation()
|
|
9859
|
-
const files = Array.from(e.dataTransfer.files).filter((f) =>
|
|
9860
|
-
f.type.startsWith('image/')
|
|
9861
|
-
)
|
|
9716
|
+
const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'))
|
|
9862
9717
|
if (files.length > 0) {
|
|
9863
9718
|
uploadImages(files)
|
|
9864
9719
|
}
|
|
9865
9720
|
},
|
|
9866
|
-
[uploadImages]
|
|
9721
|
+
[uploadImages],
|
|
9867
9722
|
)
|
|
9868
9723
|
|
|
9869
9724
|
const openFilePicker = React.useCallback(() => {
|
|
@@ -9878,7 +9733,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9878
9733
|
}
|
|
9879
9734
|
e.target.value = ''
|
|
9880
9735
|
},
|
|
9881
|
-
[uploadImages]
|
|
9736
|
+
[uploadImages],
|
|
9882
9737
|
)
|
|
9883
9738
|
|
|
9884
9739
|
return {
|
|
@@ -9888,7 +9743,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
|
|
|
9888
9743
|
handleDrop,
|
|
9889
9744
|
openFilePicker,
|
|
9890
9745
|
fileInputRef,
|
|
9891
|
-
handleFileInputChange
|
|
9746
|
+
handleFileInputChange,
|
|
9892
9747
|
}
|
|
9893
9748
|
}
|
|
9894
9749
|
`;
|
|
@@ -9912,7 +9767,7 @@ export function useLocalStorage<T>(key: string) {
|
|
|
9912
9767
|
// Silent failure for localStorage access errors
|
|
9913
9768
|
}
|
|
9914
9769
|
},
|
|
9915
|
-
[prefixedKey]
|
|
9770
|
+
[prefixedKey],
|
|
9916
9771
|
)
|
|
9917
9772
|
|
|
9918
9773
|
const getItem = React.useCallback((): T | null => {
|
|
@@ -9945,18 +9800,36 @@ export function useLocalStorage<T>(key: string) {
|
|
|
9945
9800
|
`;
|
|
9946
9801
|
}
|
|
9947
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
|
+
|
|
9948
9827
|
// src/init/templates/hooks/use-upload.ts
|
|
9949
9828
|
function useUploadHookTemplate() {
|
|
9950
9829
|
return `'use client'
|
|
9951
9830
|
|
|
9952
|
-
import type {
|
|
9953
|
-
|
|
9954
|
-
UploadProgress
|
|
9955
|
-
} from '@cms/types'
|
|
9956
|
-
import {
|
|
9957
|
-
type FileValidationConfig,
|
|
9958
|
-
validateFiles
|
|
9959
|
-
} from '@cms/utils/validation'
|
|
9831
|
+
import type { UploadFileResult, UploadProgress } from '@cms/types'
|
|
9832
|
+
import { type FileValidationConfig, validateFiles } from '@cms/utils/validation'
|
|
9960
9833
|
import { type UseMutationResult, useMutation } from '@tanstack/react-query'
|
|
9961
9834
|
import * as React from 'react'
|
|
9962
9835
|
|
|
@@ -10004,7 +9877,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10004
9877
|
onProgress,
|
|
10005
9878
|
onSuccess,
|
|
10006
9879
|
onError,
|
|
10007
|
-
prefix: defaultPrefix
|
|
9880
|
+
prefix: defaultPrefix,
|
|
10008
9881
|
} = options
|
|
10009
9882
|
|
|
10010
9883
|
const validationConfig = React.useMemo<FileValidationConfig>(() => {
|
|
@@ -10014,7 +9887,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10014
9887
|
const parsedTypes = parseAcceptTypes(accept)
|
|
10015
9888
|
if (config.allowedTypes && config.allowedTypes.length > 0) {
|
|
10016
9889
|
config.allowedTypes = [...config.allowedTypes, ...parsedTypes].filter(
|
|
10017
|
-
(v, i, a) => a.indexOf(v) === i
|
|
9890
|
+
(v, i, a) => a.indexOf(v) === i,
|
|
10018
9891
|
)
|
|
10019
9892
|
} else {
|
|
10020
9893
|
config.allowedTypes = parsedTypes
|
|
@@ -10037,7 +9910,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10037
9910
|
filename: file.name,
|
|
10038
9911
|
progress: 0,
|
|
10039
9912
|
loaded: 0,
|
|
10040
|
-
total: file.size
|
|
9913
|
+
total: file.size,
|
|
10041
9914
|
}))
|
|
10042
9915
|
setProgress(initialProgress)
|
|
10043
9916
|
onProgress?.(initialProgress)
|
|
@@ -10059,9 +9932,9 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10059
9932
|
return {
|
|
10060
9933
|
...p,
|
|
10061
9934
|
progress: newProgress,
|
|
10062
|
-
loaded: Math.floor((p.total * newProgress) / 100)
|
|
9935
|
+
loaded: Math.floor((p.total * newProgress) / 100),
|
|
10063
9936
|
}
|
|
10064
|
-
})
|
|
9937
|
+
}),
|
|
10065
9938
|
)
|
|
10066
9939
|
}, 200)
|
|
10067
9940
|
|
|
@@ -10075,7 +9948,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10075
9948
|
|
|
10076
9949
|
const response = await fetch('/api/cms/upload', {
|
|
10077
9950
|
method: 'POST',
|
|
10078
|
-
body: formData
|
|
9951
|
+
body: formData,
|
|
10079
9952
|
})
|
|
10080
9953
|
|
|
10081
9954
|
const result = (await response.json()) as UploadFileResult
|
|
@@ -10085,7 +9958,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10085
9958
|
filename: file.name,
|
|
10086
9959
|
progress: 100,
|
|
10087
9960
|
loaded: file.size,
|
|
10088
|
-
total: file.size
|
|
9961
|
+
total: file.size,
|
|
10089
9962
|
}))
|
|
10090
9963
|
setProgress(completeProgress)
|
|
10091
9964
|
onProgress?.(completeProgress)
|
|
@@ -10107,14 +9980,14 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10107
9980
|
onError: (error) => {
|
|
10108
9981
|
onError?.(error)
|
|
10109
9982
|
setProgress([])
|
|
10110
|
-
}
|
|
9983
|
+
},
|
|
10111
9984
|
})
|
|
10112
9985
|
|
|
10113
9986
|
const upload = React.useCallback(
|
|
10114
9987
|
(files: File[], prefix?: string) => {
|
|
10115
9988
|
mutation.mutate({ files, prefix })
|
|
10116
9989
|
},
|
|
10117
|
-
[mutation]
|
|
9990
|
+
[mutation],
|
|
10118
9991
|
)
|
|
10119
9992
|
|
|
10120
9993
|
const validate = React.useCallback(
|
|
@@ -10122,10 +9995,10 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
|
|
|
10122
9995
|
const result = validateFiles(files, validationConfig)
|
|
10123
9996
|
return {
|
|
10124
9997
|
valid: result.valid,
|
|
10125
|
-
errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`)
|
|
9998
|
+
errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`),
|
|
10126
9999
|
}
|
|
10127
10000
|
},
|
|
10128
|
-
[validationConfig]
|
|
10001
|
+
[validationConfig],
|
|
10129
10002
|
)
|
|
10130
10003
|
|
|
10131
10004
|
return { mutation, progress, upload, validate }
|
|
@@ -10145,7 +10018,7 @@ export function useUsers() {
|
|
|
10145
10018
|
return useQuery<UsersResponse>({
|
|
10146
10019
|
queryKey: ['users'],
|
|
10147
10020
|
queryFn: () => getUsers(),
|
|
10148
|
-
staleTime: 0
|
|
10021
|
+
staleTime: 0,
|
|
10149
10022
|
})
|
|
10150
10023
|
}
|
|
10151
10024
|
`;
|
|
@@ -10155,9 +10028,9 @@ export function useUsers() {
|
|
|
10155
10028
|
function formSettingsActionTemplate() {
|
|
10156
10029
|
return `'use server'
|
|
10157
10030
|
|
|
10158
|
-
import { eq } from 'drizzle-orm'
|
|
10159
10031
|
import db from '@cms/db'
|
|
10160
10032
|
import { formSettings } from '@cms/db/schema'
|
|
10033
|
+
import { eq } from 'drizzle-orm'
|
|
10161
10034
|
|
|
10162
10035
|
export interface FormSettingsData {
|
|
10163
10036
|
id: number
|
|
@@ -10181,9 +10054,7 @@ export interface FormSettingsResult {
|
|
|
10181
10054
|
settings?: FormSettingsData
|
|
10182
10055
|
}
|
|
10183
10056
|
|
|
10184
|
-
export async function getFormSettings(
|
|
10185
|
-
formName: string
|
|
10186
|
-
): Promise<FormSettingsData | null> {
|
|
10057
|
+
export async function getFormSettings(formName: string): Promise<FormSettingsData | null> {
|
|
10187
10058
|
try {
|
|
10188
10059
|
const [settings] = await db
|
|
10189
10060
|
.select()
|
|
@@ -10199,7 +10070,7 @@ export async function getFormSettings(
|
|
|
10199
10070
|
|
|
10200
10071
|
export async function upsertFormSettings(
|
|
10201
10072
|
formName: string,
|
|
10202
|
-
data: UpsertFormSettingsInput
|
|
10073
|
+
data: UpsertFormSettingsInput,
|
|
10203
10074
|
): Promise<FormSettingsResult> {
|
|
10204
10075
|
try {
|
|
10205
10076
|
const existing = await getFormSettings(formName)
|
|
@@ -10230,8 +10101,7 @@ export async function upsertFormSettings(
|
|
|
10230
10101
|
console.error(\`Error upserting form settings for \${formName}:\`, error)
|
|
10231
10102
|
return {
|
|
10232
10103
|
success: false,
|
|
10233
|
-
error:
|
|
10234
|
-
error instanceof Error ? error.message : 'Failed to save form settings',
|
|
10104
|
+
error: error instanceof Error ? error.message : 'Failed to save form settings',
|
|
10235
10105
|
}
|
|
10236
10106
|
}
|
|
10237
10107
|
}
|
|
@@ -10247,7 +10117,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
|
|
|
10247
10117
|
}
|
|
10248
10118
|
|
|
10249
10119
|
export async function testFormWebhook(
|
|
10250
|
-
formName: string
|
|
10120
|
+
formName: string,
|
|
10251
10121
|
): Promise<{ success: boolean; error?: string }> {
|
|
10252
10122
|
try {
|
|
10253
10123
|
const settings = await getFormSettings(formName)
|
|
@@ -10278,8 +10148,7 @@ export async function testFormWebhook(
|
|
|
10278
10148
|
} catch (error) {
|
|
10279
10149
|
return {
|
|
10280
10150
|
success: false,
|
|
10281
|
-
error:
|
|
10282
|
-
error instanceof Error ? error.message : 'Failed to send test webhook',
|
|
10151
|
+
error: error instanceof Error ? error.message : 'Failed to send test webhook',
|
|
10283
10152
|
}
|
|
10284
10153
|
}
|
|
10285
10154
|
}
|
|
@@ -10448,10 +10317,10 @@ export async function uploadImageFromUrl(
|
|
|
10448
10317
|
function usersActionTemplate() {
|
|
10449
10318
|
return `'use server'
|
|
10450
10319
|
|
|
10320
|
+
import { auth } from '@cms/auth'
|
|
10451
10321
|
import db from '@cms/db'
|
|
10452
|
-
import { user
|
|
10322
|
+
import { user } from '@cms/db/schema'
|
|
10453
10323
|
import { eq } from 'drizzle-orm'
|
|
10454
|
-
import { auth } from '@cms/auth'
|
|
10455
10324
|
|
|
10456
10325
|
export interface UserData {
|
|
10457
10326
|
id: string
|
|
@@ -10564,15 +10433,9 @@ export async function getUsers(): Promise<UsersResponse> {
|
|
|
10564
10433
|
/**
|
|
10565
10434
|
* Update a user's role
|
|
10566
10435
|
*/
|
|
10567
|
-
export async function updateUserRole(
|
|
10568
|
-
userId: string,
|
|
10569
|
-
role: string,
|
|
10570
|
-
): Promise<UpdateUserRoleResult> {
|
|
10436
|
+
export async function updateUserRole(userId: string, role: string): Promise<UpdateUserRoleResult> {
|
|
10571
10437
|
try {
|
|
10572
|
-
await db
|
|
10573
|
-
.update(user)
|
|
10574
|
-
.set({ role, updatedAt: new Date() })
|
|
10575
|
-
.where(eq(user.id, userId))
|
|
10438
|
+
await db.update(user).set({ role, updatedAt: new Date() }).where(eq(user.id, userId))
|
|
10576
10439
|
|
|
10577
10440
|
return { success: true }
|
|
10578
10441
|
} catch (error) {
|
|
@@ -10722,10 +10585,21 @@ function trimMathBlock(content: string): string {
|
|
|
10722
10585
|
const shiki = createHighlighterCoreSync({
|
|
10723
10586
|
themes: [githubDark, githubLight],
|
|
10724
10587
|
langs: [
|
|
10725
|
-
javascript,
|
|
10726
|
-
|
|
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,
|
|
10727
10601
|
],
|
|
10728
|
-
engine: createJavaScriptRegexEngine()
|
|
10602
|
+
engine: createJavaScriptRegexEngine(),
|
|
10729
10603
|
})
|
|
10730
10604
|
|
|
10731
10605
|
const loadedLangs = shiki.getLoadedLanguages()
|
|
@@ -10767,11 +10641,11 @@ const md = MarkdownIt({
|
|
|
10767
10641
|
lang: language,
|
|
10768
10642
|
themes: { light: 'github-light', dark: 'github-dark' },
|
|
10769
10643
|
defaultColor: false,
|
|
10770
|
-
transformers: [transformerNotationHighlight(), transformerNotationDiff()]
|
|
10644
|
+
transformers: [transformerNotationHighlight(), transformerNotationDiff()],
|
|
10771
10645
|
})
|
|
10772
10646
|
const escapedCode = code.replace(/</g, '<').replace(/>/g, '>')
|
|
10773
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>\`
|
|
10774
|
-
}
|
|
10648
|
+
},
|
|
10775
10649
|
})
|
|
10776
10650
|
.use(headingAnchorPlugin)
|
|
10777
10651
|
.use(dollarmath, {
|
|
@@ -10784,7 +10658,7 @@ const md = MarkdownIt({
|
|
|
10784
10658
|
return renderToString(content, {
|
|
10785
10659
|
displayMode,
|
|
10786
10660
|
throwOnError: false,
|
|
10787
|
-
strict: 'ignore'
|
|
10661
|
+
strict: 'ignore',
|
|
10788
10662
|
})
|
|
10789
10663
|
},
|
|
10790
10664
|
labelNormalizer(label: string) {
|
|
@@ -10792,7 +10666,7 @@ const md = MarkdownIt({
|
|
|
10792
10666
|
},
|
|
10793
10667
|
labelRenderer(label: string) {
|
|
10794
10668
|
return \`<a href="#\${label}" class="mathlabel" title="Permalink to this equation">\xB6</a>\`
|
|
10795
|
-
}
|
|
10669
|
+
},
|
|
10796
10670
|
})
|
|
10797
10671
|
|
|
10798
10672
|
export function renderMarkdownSync(src: string): string {
|
|
@@ -10887,7 +10761,7 @@ export interface AuthSession {
|
|
|
10887
10761
|
export enum UserRole {
|
|
10888
10762
|
ADMIN = 'admin',
|
|
10889
10763
|
EDITOR = 'editor',
|
|
10890
|
-
MEMBER = 'member'
|
|
10764
|
+
MEMBER = 'member',
|
|
10891
10765
|
}
|
|
10892
10766
|
|
|
10893
10767
|
export interface UserWithRole extends AuthUser {
|
|
@@ -10896,17 +10770,11 @@ export interface UserWithRole extends AuthUser {
|
|
|
10896
10770
|
|
|
10897
10771
|
/** Type guard to check if a value is a valid UserRole */
|
|
10898
10772
|
export function isUserRole(value: unknown): value is UserRole {
|
|
10899
|
-
return (
|
|
10900
|
-
typeof value === 'string' &&
|
|
10901
|
-
Object.values(UserRole).includes(value as UserRole)
|
|
10902
|
-
)
|
|
10773
|
+
return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
|
|
10903
10774
|
}
|
|
10904
10775
|
|
|
10905
10776
|
/** Check if user has one of the allowed roles */
|
|
10906
|
-
export function hasRequiredRole(
|
|
10907
|
-
userRole: UserRole,
|
|
10908
|
-
allowedRoles: UserRole[]
|
|
10909
|
-
): boolean {
|
|
10777
|
+
export function hasRequiredRole(userRole: UserRole, allowedRoles: UserRole[]): boolean {
|
|
10910
10778
|
return allowedRoles.includes(userRole)
|
|
10911
10779
|
}
|
|
10912
10780
|
|
|
@@ -11092,7 +10960,7 @@ export function createMetadata({
|
|
|
11092
10960
|
description,
|
|
11093
10961
|
path,
|
|
11094
10962
|
ogImage,
|
|
11095
|
-
noIndex = false
|
|
10963
|
+
noIndex = false,
|
|
11096
10964
|
}: CreateMetadataOptions): Metadata {
|
|
11097
10965
|
const metadata: Metadata = {
|
|
11098
10966
|
title,
|
|
@@ -11101,14 +10969,14 @@ export function createMetadata({
|
|
|
11101
10969
|
title,
|
|
11102
10970
|
description,
|
|
11103
10971
|
type: 'website',
|
|
11104
|
-
...(ogImage && { images: [{ url: ogImage }] })
|
|
10972
|
+
...(ogImage && { images: [{ url: ogImage }] }),
|
|
11105
10973
|
},
|
|
11106
10974
|
twitter: {
|
|
11107
10975
|
card: ogImage ? 'summary_large_image' : 'summary',
|
|
11108
10976
|
title,
|
|
11109
10977
|
description,
|
|
11110
|
-
...(ogImage && { images: [ogImage] })
|
|
11111
|
-
}
|
|
10978
|
+
...(ogImage && { images: [ogImage] }),
|
|
10979
|
+
},
|
|
11112
10980
|
}
|
|
11113
10981
|
|
|
11114
10982
|
if (path) {
|
|
@@ -11132,7 +11000,7 @@ export function generateArticleSchema({
|
|
|
11132
11000
|
imageUrl,
|
|
11133
11001
|
datePublished,
|
|
11134
11002
|
dateModified,
|
|
11135
|
-
authorName
|
|
11003
|
+
authorName,
|
|
11136
11004
|
}: {
|
|
11137
11005
|
title: string
|
|
11138
11006
|
description: string
|
|
@@ -11152,8 +11020,8 @@ export function generateArticleSchema({
|
|
|
11152
11020
|
datePublished,
|
|
11153
11021
|
...(dateModified && { dateModified }),
|
|
11154
11022
|
...(authorName && {
|
|
11155
|
-
author: { '@type': 'Person', name: authorName }
|
|
11156
|
-
})
|
|
11023
|
+
author: { '@type': 'Person', name: authorName },
|
|
11024
|
+
}),
|
|
11157
11025
|
}
|
|
11158
11026
|
}
|
|
11159
11027
|
|
|
@@ -11206,20 +11074,16 @@ function isFileTypeAllowed(file: File, allowedTypes: string[]): boolean {
|
|
|
11206
11074
|
*/
|
|
11207
11075
|
export function validateFiles(
|
|
11208
11076
|
files: File[],
|
|
11209
|
-
config: FileValidationConfig = {}
|
|
11077
|
+
config: FileValidationConfig = {},
|
|
11210
11078
|
): FileValidationResult {
|
|
11211
|
-
const {
|
|
11212
|
-
maxSizeInBytes = DEFAULT_MAX_SIZE,
|
|
11213
|
-
allowedTypes,
|
|
11214
|
-
maxFiles = DEFAULT_MAX_FILES
|
|
11215
|
-
} = config
|
|
11079
|
+
const { maxSizeInBytes = DEFAULT_MAX_SIZE, allowedTypes, maxFiles = DEFAULT_MAX_FILES } = config
|
|
11216
11080
|
|
|
11217
11081
|
const errors: FileValidationError[] = []
|
|
11218
11082
|
|
|
11219
11083
|
if (files.length > maxFiles) {
|
|
11220
11084
|
errors.push({
|
|
11221
11085
|
filename: '',
|
|
11222
|
-
error: \`Too many files. Maximum is \${maxFiles}
|
|
11086
|
+
error: \`Too many files. Maximum is \${maxFiles}.\`,
|
|
11223
11087
|
})
|
|
11224
11088
|
}
|
|
11225
11089
|
|
|
@@ -11228,14 +11092,14 @@ export function validateFiles(
|
|
|
11228
11092
|
const maxMB = Math.round(maxSizeInBytes / (1024 * 1024))
|
|
11229
11093
|
errors.push({
|
|
11230
11094
|
filename: file.name,
|
|
11231
|
-
error: \`File exceeds maximum size of \${maxMB}MB
|
|
11095
|
+
error: \`File exceeds maximum size of \${maxMB}MB.\`,
|
|
11232
11096
|
})
|
|
11233
11097
|
}
|
|
11234
11098
|
|
|
11235
11099
|
if (allowedTypes && allowedTypes.length > 0 && !isFileTypeAllowed(file, allowedTypes)) {
|
|
11236
11100
|
errors.push({
|
|
11237
11101
|
filename: file.name,
|
|
11238
|
-
error: \`File type "\${file.type || 'unknown'}" is not allowed
|
|
11102
|
+
error: \`File type "\${file.type || 'unknown'}" is not allowed.\`,
|
|
11239
11103
|
})
|
|
11240
11104
|
}
|
|
11241
11105
|
}
|
|
@@ -11289,17 +11153,15 @@ function webhookUtilTemplate() {
|
|
|
11289
11153
|
*/
|
|
11290
11154
|
export function sendWebhook(
|
|
11291
11155
|
webhookUrl: string | null | undefined,
|
|
11292
|
-
payload: Record<string, unknown
|
|
11156
|
+
payload: Record<string, unknown>,
|
|
11293
11157
|
): void {
|
|
11294
|
-
if (!webhookUrl) return
|
|
11295
|
-
// Fire-and-forget: runs in background, doesn't block
|
|
11158
|
+
if (!webhookUrl) return // Fire-and-forget: runs in background, doesn't block
|
|
11296
11159
|
;(async () => {
|
|
11297
11160
|
try {
|
|
11298
11161
|
const formData = new URLSearchParams()
|
|
11299
11162
|
for (const [key, value] of Object.entries(payload)) {
|
|
11300
11163
|
if (value === null || value === undefined) continue
|
|
11301
|
-
const stringValue =
|
|
11302
|
-
typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
11164
|
+
const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
|
|
11303
11165
|
formData.append(key, stringValue)
|
|
11304
11166
|
}
|
|
11305
11167
|
await fetch(webhookUrl, {
|
|
@@ -11327,6 +11189,7 @@ function scaffoldComponents({ cwd, config }) {
|
|
|
11327
11189
|
}
|
|
11328
11190
|
write("cms-globals.css", cmsGlobalsCssTemplate());
|
|
11329
11191
|
write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
|
|
11192
|
+
write("components/layout/cms-nav-link.tsx", cmsNavLinkTemplate());
|
|
11330
11193
|
write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
|
|
11331
11194
|
write("components/layout/cms-header.tsx", cmsHeaderTemplate());
|
|
11332
11195
|
write("components/layout/cms-search.tsx", cmsSearchTemplate());
|
|
@@ -11352,6 +11215,7 @@ function scaffoldComponents({ cwd, config }) {
|
|
|
11352
11215
|
write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
|
|
11353
11216
|
write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
|
|
11354
11217
|
write("hooks/use-users.ts", useUsersHookTemplate());
|
|
11218
|
+
write("hooks/use-mobile.ts", useMobileHookTemplate());
|
|
11355
11219
|
const projectName = detectProjectName(cwd);
|
|
11356
11220
|
write("data/cms.ts", cmsDataTemplate(projectName));
|
|
11357
11221
|
write("data/navigation.ts", navigationDataTemplate());
|
|
@@ -11588,6 +11452,7 @@ var CORE_DEPS = [
|
|
|
11588
11452
|
"nuqs",
|
|
11589
11453
|
"sonner",
|
|
11590
11454
|
// Styling utilities
|
|
11455
|
+
"geist",
|
|
11591
11456
|
"class-variance-authority",
|
|
11592
11457
|
"clsx",
|
|
11593
11458
|
"tailwind-merge",
|
|
@@ -11833,24 +11698,18 @@ import path32 from "path";
|
|
|
11833
11698
|
|
|
11834
11699
|
// src/init/templates/pages/authenticated-layout.ts
|
|
11835
11700
|
function authenticatedLayoutTemplate() {
|
|
11836
|
-
return `import {
|
|
11701
|
+
return `import { requireRole } from '@cms/auth/middleware'
|
|
11837
11702
|
import { CmsSidebar } from '@cms/components/layout/cms-sidebar'
|
|
11838
|
-
import { requireRole } from '@cms/auth/middleware'
|
|
11839
|
-
import { UserRole } from '@cms/types/auth'
|
|
11840
11703
|
import { SidebarInset, SidebarProvider } from '@cms/components/ui/sidebar'
|
|
11704
|
+
import { UserRole } from '@cms/types/auth'
|
|
11841
11705
|
|
|
11842
|
-
export default async function CmsAuthLayout({
|
|
11843
|
-
children
|
|
11844
|
-
}: {
|
|
11845
|
-
children: React.ReactNode
|
|
11846
|
-
}) {
|
|
11706
|
+
export default async function CmsAuthLayout({ children }: { children: React.ReactNode }) {
|
|
11847
11707
|
await requireRole([UserRole.ADMIN, UserRole.EDITOR])
|
|
11848
11708
|
|
|
11849
11709
|
return (
|
|
11850
11710
|
<SidebarProvider>
|
|
11851
11711
|
<CmsSidebar />
|
|
11852
11712
|
<SidebarInset>
|
|
11853
|
-
<CmsHeader />
|
|
11854
11713
|
<main>{children}</main>
|
|
11855
11714
|
</SidebarInset>
|
|
11856
11715
|
</SidebarProvider>
|
|
@@ -11863,11 +11722,17 @@ export default async function CmsAuthLayout({
|
|
|
11863
11722
|
function cmsLayoutTemplate() {
|
|
11864
11723
|
return `import '@cms/cms-globals.css'
|
|
11865
11724
|
import { CmsProviders } from '@cms/components/layout/cms-providers'
|
|
11725
|
+
import { GeistMono } from 'geist/font/mono'
|
|
11726
|
+
import { GeistSans } from 'geist/font/sans'
|
|
11866
11727
|
|
|
11867
11728
|
export default function CmsLayout({ children }: { children: React.ReactNode }) {
|
|
11868
11729
|
return (
|
|
11869
11730
|
<CmsProviders>
|
|
11870
|
-
<div
|
|
11731
|
+
<div
|
|
11732
|
+
className={\`cms-root min-h-screen antialiased \${GeistSans.variable} \${GeistMono.variable}\`}
|
|
11733
|
+
>
|
|
11734
|
+
{children}
|
|
11735
|
+
</div>
|
|
11871
11736
|
</CmsProviders>
|
|
11872
11737
|
)
|
|
11873
11738
|
}
|
|
@@ -11877,8 +11742,8 @@ export default function CmsLayout({ children }: { children: React.ReactNode }) {
|
|
|
11877
11742
|
// src/init/templates/pages/dashboard-page.ts
|
|
11878
11743
|
function dashboardPageTemplate() {
|
|
11879
11744
|
return `import { PageHeader } from '@cms/components/shared/page-header'
|
|
11880
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
|
|
11881
11745
|
import { Badge } from '@cms/components/ui/badge'
|
|
11746
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
|
|
11882
11747
|
import { FileText, Settings, Users } from 'lucide-react'
|
|
11883
11748
|
import Link from 'next/link'
|
|
11884
11749
|
|
|
@@ -11887,32 +11752,27 @@ const quickLinks = [
|
|
|
11887
11752
|
title: 'Users',
|
|
11888
11753
|
description: 'Manage admin users and roles',
|
|
11889
11754
|
href: '/cms/users',
|
|
11890
|
-
icon: Users
|
|
11755
|
+
icon: Users,
|
|
11891
11756
|
},
|
|
11892
11757
|
{
|
|
11893
11758
|
title: 'Settings',
|
|
11894
11759
|
description: 'Configure CMS settings',
|
|
11895
11760
|
href: '/cms/settings',
|
|
11896
|
-
icon: Settings
|
|
11761
|
+
icon: Settings,
|
|
11897
11762
|
},
|
|
11898
11763
|
{
|
|
11899
11764
|
title: 'Generate',
|
|
11900
11765
|
description: 'Add a new resource from a schema',
|
|
11901
11766
|
href: '#',
|
|
11902
11767
|
icon: FileText,
|
|
11903
|
-
hint: 'npx betterstart generate <schema>'
|
|
11904
|
-
}
|
|
11768
|
+
hint: 'npx betterstart generate <schema>',
|
|
11769
|
+
},
|
|
11905
11770
|
]
|
|
11906
11771
|
|
|
11907
11772
|
export default function DashboardPage() {
|
|
11908
11773
|
return (
|
|
11909
11774
|
<div className="flex flex-col">
|
|
11910
|
-
<
|
|
11911
|
-
<PageHeader
|
|
11912
|
-
title="Dashboard"
|
|
11913
|
-
description="Welcome to your CMS admin panel"
|
|
11914
|
-
/>
|
|
11915
|
-
</div>
|
|
11775
|
+
<PageHeader title="Dashboard" />
|
|
11916
11776
|
<div className="p-6 space-y-6">
|
|
11917
11777
|
<div className="grid gap-4 md:grid-cols-3">
|
|
11918
11778
|
{quickLinks.map((link) => (
|
|
@@ -11982,7 +11842,7 @@ import { LoginForm } from './login-form'
|
|
|
11982
11842
|
|
|
11983
11843
|
export const metadata: Metadata = {
|
|
11984
11844
|
title: 'CMS Login',
|
|
11985
|
-
robots: { index: false, follow: false }
|
|
11845
|
+
robots: { index: false, follow: false },
|
|
11986
11846
|
}
|
|
11987
11847
|
|
|
11988
11848
|
export default function LoginPage() {
|
|
@@ -11991,9 +11851,7 @@ export default function LoginPage() {
|
|
|
11991
11851
|
<div className="w-full max-w-sm">
|
|
11992
11852
|
<div className="mb-8 text-center">
|
|
11993
11853
|
<h1 className="text-2xl font-semibold tracking-tight">CMS</h1>
|
|
11994
|
-
<p className="text-muted-foreground text-sm mt-1">
|
|
11995
|
-
Sign in to access the admin panel
|
|
11996
|
-
</p>
|
|
11854
|
+
<p className="text-muted-foreground text-sm mt-1">Sign in to access the admin panel</p>
|
|
11997
11855
|
</div>
|
|
11998
11856
|
<LoginForm />
|
|
11999
11857
|
</div>
|
|
@@ -12028,7 +11886,7 @@ export function LoginForm() {
|
|
|
12028
11886
|
try {
|
|
12029
11887
|
const result = await authClient.signIn.email({
|
|
12030
11888
|
email,
|
|
12031
|
-
password
|
|
11889
|
+
password,
|
|
12032
11890
|
})
|
|
12033
11891
|
|
|
12034
11892
|
if (result.error) {
|
|
@@ -12048,9 +11906,7 @@ export function LoginForm() {
|
|
|
12048
11906
|
return (
|
|
12049
11907
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
12050
11908
|
{error && (
|
|
12051
|
-
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
|
|
12052
|
-
{error}
|
|
12053
|
-
</div>
|
|
11909
|
+
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">{error}</div>
|
|
12054
11910
|
)}
|
|
12055
11911
|
|
|
12056
11912
|
<div className="space-y-2">
|
|
@@ -12095,6 +11951,7 @@ export function LoginForm() {
|
|
|
12095
11951
|
function createUserDialogTemplate() {
|
|
12096
11952
|
return `'use client'
|
|
12097
11953
|
|
|
11954
|
+
import { createUser } from '@cms/actions/users'
|
|
12098
11955
|
import { Button } from '@cms/components/ui/button'
|
|
12099
11956
|
import {
|
|
12100
11957
|
Dialog,
|
|
@@ -12102,11 +11959,10 @@ import {
|
|
|
12102
11959
|
DialogDescription,
|
|
12103
11960
|
DialogHeader,
|
|
12104
11961
|
DialogTitle,
|
|
12105
|
-
DialogTrigger
|
|
11962
|
+
DialogTrigger,
|
|
12106
11963
|
} from '@cms/components/ui/dialog'
|
|
12107
11964
|
import { Input } from '@cms/components/ui/input'
|
|
12108
11965
|
import { Label } from '@cms/components/ui/label'
|
|
12109
|
-
import { createUser } from '@cms/actions/users'
|
|
12110
11966
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12111
11967
|
import { Loader2, UserPlus } from 'lucide-react'
|
|
12112
11968
|
import * as React from 'react'
|
|
@@ -12216,6 +12072,7 @@ export function CreateUserDialog() {
|
|
|
12216
12072
|
function editRoleDialogTemplate() {
|
|
12217
12073
|
return `'use client'
|
|
12218
12074
|
|
|
12075
|
+
import { updateUserRole } from '@cms/actions/users'
|
|
12219
12076
|
import { Button } from '@cms/components/ui/button'
|
|
12220
12077
|
import {
|
|
12221
12078
|
Dialog,
|
|
@@ -12223,7 +12080,7 @@ import {
|
|
|
12223
12080
|
DialogDescription,
|
|
12224
12081
|
DialogHeader,
|
|
12225
12082
|
DialogTitle,
|
|
12226
|
-
DialogTrigger
|
|
12083
|
+
DialogTrigger,
|
|
12227
12084
|
} from '@cms/components/ui/dialog'
|
|
12228
12085
|
import { Label } from '@cms/components/ui/label'
|
|
12229
12086
|
import {
|
|
@@ -12231,9 +12088,8 @@ import {
|
|
|
12231
12088
|
SelectContent,
|
|
12232
12089
|
SelectItem,
|
|
12233
12090
|
SelectTrigger,
|
|
12234
|
-
SelectValue
|
|
12091
|
+
SelectValue,
|
|
12235
12092
|
} from '@cms/components/ui/select'
|
|
12236
|
-
import { updateUserRole } from '@cms/actions/users'
|
|
12237
12093
|
import { UserRole } from '@cms/types/auth'
|
|
12238
12094
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12239
12095
|
import { Loader2 } from 'lucide-react'
|
|
@@ -12247,12 +12103,7 @@ interface EditRoleDialogProps {
|
|
|
12247
12103
|
children: React.ReactNode
|
|
12248
12104
|
}
|
|
12249
12105
|
|
|
12250
|
-
export function EditRoleDialog({
|
|
12251
|
-
userId,
|
|
12252
|
-
currentRole,
|
|
12253
|
-
userName,
|
|
12254
|
-
children
|
|
12255
|
-
}: EditRoleDialogProps) {
|
|
12106
|
+
export function EditRoleDialog({ userId, currentRole, userName, children }: EditRoleDialogProps) {
|
|
12256
12107
|
const [open, setOpen] = React.useState(false)
|
|
12257
12108
|
const [role, setRole] = React.useState(currentRole)
|
|
12258
12109
|
const [isPending, startTransition] = React.useTransition()
|
|
@@ -12281,9 +12132,7 @@ export function EditRoleDialog({
|
|
|
12281
12132
|
<DialogContent className="sm:max-w-[350px]">
|
|
12282
12133
|
<DialogHeader>
|
|
12283
12134
|
<DialogTitle>Edit Role</DialogTitle>
|
|
12284
|
-
<DialogDescription>
|
|
12285
|
-
Change the role for {userName}
|
|
12286
|
-
</DialogDescription>
|
|
12135
|
+
<DialogDescription>Change the role for {userName}</DialogDescription>
|
|
12287
12136
|
</DialogHeader>
|
|
12288
12137
|
<div className="space-y-4">
|
|
12289
12138
|
<div className="space-y-2">
|
|
@@ -12300,17 +12149,10 @@ export function EditRoleDialog({
|
|
|
12300
12149
|
</Select>
|
|
12301
12150
|
</div>
|
|
12302
12151
|
<div className="flex justify-end gap-2">
|
|
12303
|
-
<Button
|
|
12304
|
-
variant="outline"
|
|
12305
|
-
onClick={() => setOpen(false)}
|
|
12306
|
-
disabled={isPending}
|
|
12307
|
-
>
|
|
12152
|
+
<Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
|
12308
12153
|
Cancel
|
|
12309
12154
|
</Button>
|
|
12310
|
-
<Button
|
|
12311
|
-
onClick={handleSave}
|
|
12312
|
-
disabled={isPending || role === currentRole}
|
|
12313
|
-
>
|
|
12155
|
+
<Button onClick={handleSave} disabled={isPending || role === currentRole}>
|
|
12314
12156
|
{isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
|
|
12315
12157
|
Save
|
|
12316
12158
|
</Button>
|
|
@@ -12327,11 +12169,7 @@ export function EditRoleDialog({
|
|
|
12327
12169
|
function usersColumnsTemplate() {
|
|
12328
12170
|
return `'use client'
|
|
12329
12171
|
|
|
12330
|
-
import
|
|
12331
|
-
import {
|
|
12332
|
-
Avatar,
|
|
12333
|
-
AvatarFallback
|
|
12334
|
-
} from '@cms/components/ui/avatar'
|
|
12172
|
+
import { deleteUser } from '@cms/actions/users'
|
|
12335
12173
|
import {
|
|
12336
12174
|
AlertDialog,
|
|
12337
12175
|
AlertDialogAction,
|
|
@@ -12343,6 +12181,7 @@ import {
|
|
|
12343
12181
|
AlertDialogTitle,
|
|
12344
12182
|
AlertDialogTrigger,
|
|
12345
12183
|
} from '@cms/components/ui/alert-dialog'
|
|
12184
|
+
import { Avatar, AvatarFallback } from '@cms/components/ui/avatar'
|
|
12346
12185
|
import { Badge } from '@cms/components/ui/badge'
|
|
12347
12186
|
import { Button } from '@cms/components/ui/button'
|
|
12348
12187
|
import {
|
|
@@ -12351,14 +12190,14 @@ import {
|
|
|
12351
12190
|
DropdownMenuItem,
|
|
12352
12191
|
DropdownMenuLabel,
|
|
12353
12192
|
DropdownMenuSeparator,
|
|
12354
|
-
DropdownMenuTrigger
|
|
12193
|
+
DropdownMenuTrigger,
|
|
12355
12194
|
} from '@cms/components/ui/dropdown-menu'
|
|
12356
12195
|
import type { UserData } from '@cms/types/auth'
|
|
12357
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
12358
12196
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12197
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
12359
12198
|
import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
|
|
12199
|
+
import React from 'react'
|
|
12360
12200
|
import { toast } from 'sonner'
|
|
12361
|
-
import { deleteUser } from '@cms/actions/users'
|
|
12362
12201
|
import { EditRoleDialog } from './edit-role-dialog'
|
|
12363
12202
|
|
|
12364
12203
|
function getInitials(nameOrEmail: string): string {
|
|
@@ -12409,10 +12248,7 @@ function DeleteUserAction({
|
|
|
12409
12248
|
return (
|
|
12410
12249
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
12411
12250
|
<AlertDialogTrigger asChild>
|
|
12412
|
-
<DropdownMenuItem
|
|
12413
|
-
className="text-destructive"
|
|
12414
|
-
onSelect={(e) => e.preventDefault()}
|
|
12415
|
-
>
|
|
12251
|
+
<DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
|
|
12416
12252
|
<Trash className="size-4 mr-2" />
|
|
12417
12253
|
Delete user
|
|
12418
12254
|
</DropdownMenuItem>
|
|
@@ -12421,8 +12257,8 @@ function DeleteUserAction({
|
|
|
12421
12257
|
<AlertDialogHeader>
|
|
12422
12258
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
12423
12259
|
<AlertDialogDescription>
|
|
12424
|
-
This action cannot be undone. This will permanently delete{' '}
|
|
12425
|
-
|
|
12260
|
+
This action cannot be undone. This will permanently delete <strong>{userName}</strong>{' '}
|
|
12261
|
+
and all of their data.
|
|
12426
12262
|
</AlertDialogDescription>
|
|
12427
12263
|
</AlertDialogHeader>
|
|
12428
12264
|
<AlertDialogFooter>
|
|
@@ -12470,7 +12306,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12470
12306
|
</div>
|
|
12471
12307
|
</div>
|
|
12472
12308
|
)
|
|
12473
|
-
}
|
|
12309
|
+
},
|
|
12474
12310
|
},
|
|
12475
12311
|
{
|
|
12476
12312
|
accessorKey: 'emailVerified',
|
|
@@ -12482,7 +12318,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12482
12318
|
{verified ? 'Verified' : 'Unverified'}
|
|
12483
12319
|
</Badge>
|
|
12484
12320
|
)
|
|
12485
|
-
}
|
|
12321
|
+
},
|
|
12486
12322
|
},
|
|
12487
12323
|
{
|
|
12488
12324
|
accessorKey: 'role',
|
|
@@ -12512,7 +12348,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12512
12348
|
</Badge>
|
|
12513
12349
|
</EditRoleDialog>
|
|
12514
12350
|
)
|
|
12515
|
-
}
|
|
12351
|
+
},
|
|
12516
12352
|
},
|
|
12517
12353
|
{
|
|
12518
12354
|
accessorKey: 'createdAt',
|
|
@@ -12533,11 +12369,11 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12533
12369
|
{date.toLocaleDateString('en-US', {
|
|
12534
12370
|
month: 'short',
|
|
12535
12371
|
day: 'numeric',
|
|
12536
|
-
year: 'numeric'
|
|
12372
|
+
year: 'numeric',
|
|
12537
12373
|
})}
|
|
12538
12374
|
</div>
|
|
12539
12375
|
)
|
|
12540
|
-
}
|
|
12376
|
+
},
|
|
12541
12377
|
},
|
|
12542
12378
|
{
|
|
12543
12379
|
id: 'actions',
|
|
@@ -12556,9 +12392,7 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12556
12392
|
</DropdownMenuTrigger>
|
|
12557
12393
|
<DropdownMenuContent align="end">
|
|
12558
12394
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
12559
|
-
<DropdownMenuItem
|
|
12560
|
-
onClick={() => navigator.clipboard.writeText(row.original.id)}
|
|
12561
|
-
>
|
|
12395
|
+
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
|
|
12562
12396
|
Copy user ID
|
|
12563
12397
|
</DropdownMenuItem>
|
|
12564
12398
|
<DropdownMenuSeparator />
|
|
@@ -12571,8 +12405,8 @@ export const columns: ColumnDef<UserData>[] = [
|
|
|
12571
12405
|
</DropdownMenu>
|
|
12572
12406
|
</div>
|
|
12573
12407
|
)
|
|
12574
|
-
}
|
|
12575
|
-
}
|
|
12408
|
+
},
|
|
12409
|
+
},
|
|
12576
12410
|
]
|
|
12577
12411
|
`;
|
|
12578
12412
|
}
|
|
@@ -12588,10 +12422,7 @@ export default function UsersPage() {
|
|
|
12588
12422
|
return (
|
|
12589
12423
|
<div className="flex flex-col">
|
|
12590
12424
|
<div className="flex items-center justify-between bg-card px-6 py-4 border-b">
|
|
12591
|
-
<PageHeader
|
|
12592
|
-
title="Users"
|
|
12593
|
-
description="Manage all CMS users"
|
|
12594
|
-
>
|
|
12425
|
+
<PageHeader title="Users">
|
|
12595
12426
|
<CreateUserDialog />
|
|
12596
12427
|
</PageHeader>
|
|
12597
12428
|
</div>
|
|
@@ -12608,6 +12439,18 @@ export default function UsersPage() {
|
|
|
12608
12439
|
function usersTableTemplate() {
|
|
12609
12440
|
return `'use client'
|
|
12610
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'
|
|
12611
12454
|
import {
|
|
12612
12455
|
type ColumnDef,
|
|
12613
12456
|
type ColumnFiltersState,
|
|
@@ -12618,21 +12461,9 @@ import {
|
|
|
12618
12461
|
getSortedRowModel,
|
|
12619
12462
|
type SortingState,
|
|
12620
12463
|
useReactTable,
|
|
12621
|
-
type VisibilityState
|
|
12464
|
+
type VisibilityState,
|
|
12622
12465
|
} from '@tanstack/react-table'
|
|
12623
12466
|
import * as React from 'react'
|
|
12624
|
-
import { Button } from '@cms/components/ui/button'
|
|
12625
|
-
import {
|
|
12626
|
-
Table,
|
|
12627
|
-
TableBody,
|
|
12628
|
-
TableCell,
|
|
12629
|
-
TableHead,
|
|
12630
|
-
TableHeader,
|
|
12631
|
-
TableRow
|
|
12632
|
-
} from '@cms/components/ui/table'
|
|
12633
|
-
import { useUsers } from '@cms/hooks/use-users'
|
|
12634
|
-
import { authClient } from '@cms/auth/client'
|
|
12635
|
-
import type { UserData } from '@cms/types/auth'
|
|
12636
12467
|
|
|
12637
12468
|
interface UsersTableProps<TValue> {
|
|
12638
12469
|
columns: ColumnDef<UserData, TValue>[]
|
|
@@ -12662,11 +12493,11 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
|
|
|
12662
12493
|
email: session.user.email,
|
|
12663
12494
|
name: session.user.name,
|
|
12664
12495
|
image: session.user.image,
|
|
12665
|
-
role: (session.user as { role?: string }).role || 'member'
|
|
12496
|
+
role: (session.user as { role?: string }).role || 'member',
|
|
12666
12497
|
}
|
|
12667
|
-
: null
|
|
12498
|
+
: null,
|
|
12668
12499
|
},
|
|
12669
|
-
state: { sorting, columnFilters, columnVisibility }
|
|
12500
|
+
state: { sorting, columnFilters, columnVisibility },
|
|
12670
12501
|
})
|
|
12671
12502
|
|
|
12672
12503
|
return (
|
|
@@ -13606,24 +13437,29 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
|
|
|
13606
13437
|
const { execFile } = await import("child_process");
|
|
13607
13438
|
const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
|
|
13608
13439
|
const runSeed2 = (overwrite) => new Promise((resolve, reject) => {
|
|
13609
|
-
execFile(
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
}
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
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
|
+
}
|
|
13625
13461
|
}
|
|
13626
|
-
|
|
13462
|
+
);
|
|
13627
13463
|
});
|
|
13628
13464
|
const spinner5 = clack.spinner();
|
|
13629
13465
|
spinner5.start("Creating admin user...");
|
|
@@ -13674,7 +13510,7 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
|
|
|
13674
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(
|
|
13675
13511
|
"--database-url <url>",
|
|
13676
13512
|
"PostgreSQL database connection string (postgres:// or postgresql://)"
|
|
13677
|
-
).action(
|
|
13513
|
+
).option("--force", "Overwrite all existing CMS files (nuclear option)").action(
|
|
13678
13514
|
async (name, options) => {
|
|
13679
13515
|
p4.intro(pc2.bgCyan(pc2.black(" BetterStart CMS ")));
|
|
13680
13516
|
let cwd = process.cwd();
|
|
@@ -13689,9 +13525,35 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13689
13525
|
p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
|
|
13690
13526
|
process.exit(1);
|
|
13691
13527
|
}
|
|
13692
|
-
if (
|
|
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
|
+
}
|
|
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) {
|
|
13693
13551
|
const conflictLines = project.conflicts.map((c) => `${pc2.yellow("\u25B2")} ${c}`);
|
|
13694
|
-
conflictLines.push(
|
|
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
|
+
);
|
|
13695
13557
|
p4.note(conflictLines.join("\n"), pc2.yellow("Conflicts"));
|
|
13696
13558
|
if (!options.yes) {
|
|
13697
13559
|
const proceed = await p4.confirm({
|
|
@@ -13862,13 +13724,17 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13862
13724
|
p4.note(noteLines.join("\n"), "Scaffolded CMS");
|
|
13863
13725
|
const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
|
|
13864
13726
|
if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
|
|
13865
|
-
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) {
|
|
13866
13732
|
const overwrite = await p4.confirm({
|
|
13867
13733
|
message: "drizzle.config.ts already exists. Overwrite with latest version?",
|
|
13868
13734
|
initialValue: true
|
|
13869
13735
|
});
|
|
13870
13736
|
if (!p4.isCancel(overwrite) && overwrite) {
|
|
13871
|
-
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-
|
|
13737
|
+
const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
|
|
13872
13738
|
fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
|
|
13873
13739
|
p4.log.success("Updated drizzle.config.ts");
|
|
13874
13740
|
}
|
|
@@ -13985,7 +13851,12 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13985
13851
|
seedEmail = credentials.email;
|
|
13986
13852
|
seedPassword = credentials.password;
|
|
13987
13853
|
s.start("Creating admin user");
|
|
13988
|
-
let seedResult = await runSeed(
|
|
13854
|
+
let seedResult = await runSeed(
|
|
13855
|
+
cwd,
|
|
13856
|
+
config.paths?.cms ?? "./cms",
|
|
13857
|
+
credentials.email,
|
|
13858
|
+
credentials.password
|
|
13859
|
+
);
|
|
13989
13860
|
if (seedResult.existingUser) {
|
|
13990
13861
|
s.stop(`${pc2.yellow("\u25B2")} Admin user already exists (${seedResult.existingUser})`);
|
|
13991
13862
|
const replace = await p4.confirm({
|
|
@@ -13994,7 +13865,13 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
|
|
|
13994
13865
|
});
|
|
13995
13866
|
if (!p4.isCancel(replace) && replace) {
|
|
13996
13867
|
s.start("Replacing admin user");
|
|
13997
|
-
seedResult = await runSeed(
|
|
13868
|
+
seedResult = await runSeed(
|
|
13869
|
+
cwd,
|
|
13870
|
+
config.paths?.cms ?? "./cms",
|
|
13871
|
+
credentials.email,
|
|
13872
|
+
credentials.password,
|
|
13873
|
+
true
|
|
13874
|
+
);
|
|
13998
13875
|
} else {
|
|
13999
13876
|
seedSuccess = true;
|
|
14000
13877
|
}
|
|
@@ -14187,8 +14064,7 @@ ${stderr}`;
|
|
|
14187
14064
|
if (combined.includes("Failed to create user")) return "Auth API failed to create user";
|
|
14188
14065
|
if (combined.includes("ECONNREFUSED") || combined.includes("connection refused"))
|
|
14189
14066
|
return "Could not connect to database";
|
|
14190
|
-
if (combined.includes("BETTERSTART_DATABASE_URL"))
|
|
14191
|
-
return "Database URL is missing or invalid";
|
|
14067
|
+
if (combined.includes("BETTERSTART_DATABASE_URL")) return "Database URL is missing or invalid";
|
|
14192
14068
|
if (combined.includes("password authentication failed"))
|
|
14193
14069
|
return "Database authentication failed \u2014 check your connection string";
|
|
14194
14070
|
if (combined.includes("does not exist") && combined.includes("relation"))
|
|
@@ -14443,41 +14319,12 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
|
|
|
14443
14319
|
console.log("");
|
|
14444
14320
|
});
|
|
14445
14321
|
|
|
14446
|
-
// src/commands/update-deps.ts
|
|
14447
|
-
import path39 from "path";
|
|
14448
|
-
import * as clack2 from "@clack/prompts";
|
|
14449
|
-
import { Command as Command5 } from "commander";
|
|
14450
|
-
var updateDepsCommand = new Command5("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
|
|
14451
|
-
const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
|
|
14452
|
-
clack2.intro("BetterStart Update Dependencies");
|
|
14453
|
-
const pm = detectPackageManager(cwd);
|
|
14454
|
-
clack2.log.info(`Package manager: ${pm}`);
|
|
14455
|
-
const config = await resolveConfig(cwd);
|
|
14456
|
-
const includeEmail = config.features?.email ?? true;
|
|
14457
|
-
const s = clack2.spinner();
|
|
14458
|
-
s.start("Installing dependencies...");
|
|
14459
|
-
const result = await installDependenciesAsync({
|
|
14460
|
-
cwd,
|
|
14461
|
-
pm,
|
|
14462
|
-
includeEmail,
|
|
14463
|
-
includeBiome: false
|
|
14464
|
-
});
|
|
14465
|
-
if (result.success) {
|
|
14466
|
-
s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
|
|
14467
|
-
} else {
|
|
14468
|
-
s.stop("Dependency install failed");
|
|
14469
|
-
clack2.log.error(result.error ?? "Unknown error");
|
|
14470
|
-
process.exit(1);
|
|
14471
|
-
}
|
|
14472
|
-
clack2.outro("Dependencies updated");
|
|
14473
|
-
});
|
|
14474
|
-
|
|
14475
14322
|
// src/commands/uninstall.ts
|
|
14476
14323
|
import fs35 from "fs";
|
|
14477
|
-
import
|
|
14324
|
+
import path39 from "path";
|
|
14478
14325
|
import * as p5 from "@clack/prompts";
|
|
14326
|
+
import { Command as Command5 } from "commander";
|
|
14479
14327
|
import pc3 from "picocolors";
|
|
14480
|
-
import { Command as Command6 } from "commander";
|
|
14481
14328
|
|
|
14482
14329
|
// src/commands/uninstall-cleaners.ts
|
|
14483
14330
|
import fs34 from "fs";
|
|
@@ -14534,7 +14381,7 @@ function cleanTsconfig(tsconfigPath) {
|
|
|
14534
14381
|
}
|
|
14535
14382
|
if (removed.length === 0) return [];
|
|
14536
14383
|
if (Object.keys(paths).length === 0) {
|
|
14537
|
-
|
|
14384
|
+
compilerOptions.paths = void 0;
|
|
14538
14385
|
} else {
|
|
14539
14386
|
compilerOptions.paths = paths;
|
|
14540
14387
|
}
|
|
@@ -14588,14 +14435,14 @@ function cleanEnvFile(envPath) {
|
|
|
14588
14435
|
}
|
|
14589
14436
|
if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
|
|
14590
14437
|
const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
|
|
14591
|
-
if (nextNonEmpty
|
|
14438
|
+
if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
|
|
14592
14439
|
continue;
|
|
14593
14440
|
}
|
|
14594
14441
|
}
|
|
14595
14442
|
kept.push(line);
|
|
14596
14443
|
}
|
|
14597
14444
|
if (removed.length === 0) return [];
|
|
14598
|
-
|
|
14445
|
+
const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
14599
14446
|
if (result === "") {
|
|
14600
14447
|
fs34.unlinkSync(envPath);
|
|
14601
14448
|
} else {
|
|
@@ -14625,7 +14472,7 @@ function findMainCss2(cwd) {
|
|
|
14625
14472
|
"globals.css"
|
|
14626
14473
|
];
|
|
14627
14474
|
for (const candidate of candidates) {
|
|
14628
|
-
const filePath =
|
|
14475
|
+
const filePath = path39.join(cwd, candidate);
|
|
14629
14476
|
if (fs35.existsSync(filePath)) return filePath;
|
|
14630
14477
|
}
|
|
14631
14478
|
return void 0;
|
|
@@ -14641,11 +14488,11 @@ function isCLICreatedBiome(biomePath) {
|
|
|
14641
14488
|
}
|
|
14642
14489
|
function buildUninstallPlan(cwd) {
|
|
14643
14490
|
const steps = [];
|
|
14644
|
-
const hasSrc = fs35.existsSync(
|
|
14491
|
+
const hasSrc = fs35.existsSync(path39.join(cwd, "src"));
|
|
14645
14492
|
const appBase = hasSrc ? "src/app" : "app";
|
|
14646
14493
|
const dirs = [];
|
|
14647
|
-
const cmsDir =
|
|
14648
|
-
const cmsRouteGroup =
|
|
14494
|
+
const cmsDir = path39.join(cwd, "cms");
|
|
14495
|
+
const cmsRouteGroup = path39.join(cwd, appBase, "(cms)");
|
|
14649
14496
|
if (fs35.existsSync(cmsDir)) dirs.push("cms/");
|
|
14650
14497
|
if (fs35.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
|
|
14651
14498
|
if (dirs.length > 0) {
|
|
@@ -14663,9 +14510,9 @@ function buildUninstallPlan(cwd) {
|
|
|
14663
14510
|
const configFiles = [];
|
|
14664
14511
|
const configPaths = [];
|
|
14665
14512
|
const candidates = [
|
|
14666
|
-
["cms.config.ts",
|
|
14667
|
-
["drizzle.config.ts",
|
|
14668
|
-
["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")]
|
|
14669
14516
|
];
|
|
14670
14517
|
for (const [label, fullPath] of candidates) {
|
|
14671
14518
|
if (fs35.existsSync(fullPath)) {
|
|
@@ -14673,7 +14520,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14673
14520
|
configPaths.push(fullPath);
|
|
14674
14521
|
}
|
|
14675
14522
|
}
|
|
14676
|
-
const biomePath =
|
|
14523
|
+
const biomePath = path39.join(cwd, "biome.json");
|
|
14677
14524
|
if (isCLICreatedBiome(biomePath)) {
|
|
14678
14525
|
configFiles.push("biome.json (CLI-created)");
|
|
14679
14526
|
configPaths.push(biomePath);
|
|
@@ -14691,7 +14538,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14691
14538
|
}
|
|
14692
14539
|
});
|
|
14693
14540
|
}
|
|
14694
|
-
const tsconfigPath =
|
|
14541
|
+
const tsconfigPath = path39.join(cwd, "tsconfig.json");
|
|
14695
14542
|
if (fs35.existsSync(tsconfigPath)) {
|
|
14696
14543
|
const content = fs35.readFileSync(tsconfigPath, "utf-8");
|
|
14697
14544
|
const aliasMatches = content.match(/"@cms\//g);
|
|
@@ -14713,7 +14560,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14713
14560
|
const cssContent = fs35.readFileSync(cssFile, "utf-8");
|
|
14714
14561
|
const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
|
|
14715
14562
|
if (sourceLines.length > 0) {
|
|
14716
|
-
const relCss =
|
|
14563
|
+
const relCss = path39.relative(cwd, cssFile);
|
|
14717
14564
|
steps.push({
|
|
14718
14565
|
label: `CSS @source lines (${relCss})`,
|
|
14719
14566
|
items: [`@source lines in ${relCss}`],
|
|
@@ -14725,7 +14572,7 @@ function buildUninstallPlan(cwd) {
|
|
|
14725
14572
|
});
|
|
14726
14573
|
}
|
|
14727
14574
|
}
|
|
14728
|
-
const envPath =
|
|
14575
|
+
const envPath = path39.join(cwd, ".env.local");
|
|
14729
14576
|
if (fs35.existsSync(envPath)) {
|
|
14730
14577
|
const envContent = fs35.readFileSync(envPath, "utf-8");
|
|
14731
14578
|
const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
|
|
@@ -14743,12 +14590,12 @@ function buildUninstallPlan(cwd) {
|
|
|
14743
14590
|
}
|
|
14744
14591
|
return steps;
|
|
14745
14592
|
}
|
|
14746
|
-
var uninstallCommand = new
|
|
14747
|
-
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();
|
|
14748
14595
|
p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
|
|
14749
14596
|
const steps = buildUninstallPlan(cwd);
|
|
14750
14597
|
if (steps.length === 0) {
|
|
14751
|
-
p5.log.success(pc3.green("\u2713")
|
|
14598
|
+
p5.log.success(`${pc3.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
|
|
14752
14599
|
p5.outro("Done");
|
|
14753
14600
|
return;
|
|
14754
14601
|
}
|
|
@@ -14776,13 +14623,39 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
|
|
|
14776
14623
|
}
|
|
14777
14624
|
const parts = steps.map((step) => `${step.count} ${step.unit}`);
|
|
14778
14625
|
s.stop(`Removed ${parts.join(", ")}`);
|
|
14779
|
-
p5.note(
|
|
14780
|
-
pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."),
|
|
14781
|
-
"Next steps"
|
|
14782
|
-
);
|
|
14626
|
+
p5.note(pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
|
|
14783
14627
|
p5.outro("Uninstall complete");
|
|
14784
14628
|
});
|
|
14785
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`);
|
|
14651
|
+
} else {
|
|
14652
|
+
s.stop("Dependency install failed");
|
|
14653
|
+
clack2.log.error(result.error ?? "Unknown error");
|
|
14654
|
+
process.exit(1);
|
|
14655
|
+
}
|
|
14656
|
+
clack2.outro("Dependencies updated");
|
|
14657
|
+
});
|
|
14658
|
+
|
|
14786
14659
|
// src/commands/update-styles.ts
|
|
14787
14660
|
import fs36 from "fs";
|
|
14788
14661
|
import path41 from "path";
|