@tower_74/cms-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/package.json +49 -0
- package/src/components/AppContent.vue +21 -0
- package/src/components/AppLogoIcon.vue +24 -0
- package/src/components/AppShell.vue +37 -0
- package/src/components/AppearanceTabs.vue +37 -0
- package/src/components/AuthBar.vue +58 -0
- package/src/components/BlockEditor.vue +95 -0
- package/src/components/DeleteUser.vue +87 -0
- package/src/components/FieldBuilder.vue +105 -0
- package/src/components/Heading.vue +20 -0
- package/src/components/HeadingSmall.vue +17 -0
- package/src/components/Icon.vue +30 -0
- package/src/components/InputError.vue +13 -0
- package/src/components/MenuItemsEditor.vue +59 -0
- package/src/components/NavUser.vue +30 -0
- package/src/components/Pagination.vue +28 -0
- package/src/components/PlaceholderPattern.vue +16 -0
- package/src/components/Seo.vue +28 -0
- package/src/components/TextLink.vue +24 -0
- package/src/components/UserInfo.vue +34 -0
- package/src/components/UserMenuContent.vue +37 -0
- package/src/components/commerce/OptionsEditor.vue +55 -0
- package/src/components/commerce/VariantsEditor.vue +71 -0
- package/src/components/ui/avatar/Avatar.vue +24 -0
- package/src/components/ui/avatar/AvatarFallback.vue +11 -0
- package/src/components/ui/avatar/AvatarImage.vue +9 -0
- package/src/components/ui/avatar/index.ts +24 -0
- package/src/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +18 -0
- package/src/components/ui/breadcrumb/BreadcrumbItem.vue +14 -0
- package/src/components/ui/breadcrumb/BreadcrumbLink.vue +15 -0
- package/src/components/ui/breadcrumb/BreadcrumbList.vue +14 -0
- package/src/components/ui/breadcrumb/BreadcrumbPage.vue +14 -0
- package/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +17 -0
- package/src/components/ui/breadcrumb/index.ts +7 -0
- package/src/components/ui/button/Button.vue +22 -0
- package/src/components/ui/button/index.ts +31 -0
- package/src/components/ui/card/Card.vue +14 -0
- package/src/components/ui/card/CardContent.vue +14 -0
- package/src/components/ui/card/CardDescription.vue +14 -0
- package/src/components/ui/card/CardFooter.vue +14 -0
- package/src/components/ui/card/CardHeader.vue +14 -0
- package/src/components/ui/card/CardTitle.vue +14 -0
- package/src/components/ui/card/index.ts +6 -0
- package/src/components/ui/checkbox/Checkbox.vue +36 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/collapsible/Collapsible.vue +15 -0
- package/src/components/ui/collapsible/CollapsibleContent.vue +14 -0
- package/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
- package/src/components/ui/collapsible/index.ts +3 -0
- package/src/components/ui/dialog/Dialog.vue +14 -0
- package/src/components/ui/dialog/DialogClose.vue +11 -0
- package/src/components/ui/dialog/DialogContent.vue +51 -0
- package/src/components/ui/dialog/DialogDescription.vue +21 -0
- package/src/components/ui/dialog/DialogFooter.vue +12 -0
- package/src/components/ui/dialog/DialogHeader.vue +14 -0
- package/src/components/ui/dialog/DialogScrollContent.vue +59 -0
- package/src/components/ui/dialog/DialogTitle.vue +21 -0
- package/src/components/ui/dialog/DialogTrigger.vue +11 -0
- package/src/components/ui/dialog/index.ts +9 -0
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +42 -0
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +40 -0
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +30 -0
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +21 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +43 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +21 -0
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +30 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
- package/src/components/ui/dropdown-menu/index.ts +16 -0
- package/src/components/ui/input/Input.vue +32 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/label/Label.vue +22 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/navigation-menu/NavigationMenu.vue +25 -0
- package/src/components/ui/navigation-menu/NavigationMenuContent.vue +31 -0
- package/src/components/ui/navigation-menu/NavigationMenuIndicator.vue +29 -0
- package/src/components/ui/navigation-menu/NavigationMenuItem.vue +11 -0
- package/src/components/ui/navigation-menu/NavigationMenuLink.vue +14 -0
- package/src/components/ui/navigation-menu/NavigationMenuList.vue +21 -0
- package/src/components/ui/navigation-menu/NavigationMenuTrigger.vue +24 -0
- package/src/components/ui/navigation-menu/NavigationMenuViewport.vue +29 -0
- package/src/components/ui/navigation-menu/index.ts +14 -0
- package/src/components/ui/separator/Separator.vue +31 -0
- package/src/components/ui/separator/index.ts +1 -0
- package/src/components/ui/sheet/Sheet.vue +14 -0
- package/src/components/ui/sheet/SheetClose.vue +11 -0
- package/src/components/ui/sheet/SheetContent.vue +53 -0
- package/src/components/ui/sheet/SheetDescription.vue +19 -0
- package/src/components/ui/sheet/SheetFooter.vue +12 -0
- package/src/components/ui/sheet/SheetHeader.vue +12 -0
- package/src/components/ui/sheet/SheetTitle.vue +19 -0
- package/src/components/ui/sheet/SheetTrigger.vue +11 -0
- package/src/components/ui/sheet/index.ts +29 -0
- package/src/components/ui/sidebar/Sidebar.vue +99 -0
- package/src/components/ui/sidebar/SidebarContent.vue +17 -0
- package/src/components/ui/sidebar/SidebarFooter.vue +14 -0
- package/src/components/ui/sidebar/SidebarGroup.vue +14 -0
- package/src/components/ui/sidebar/SidebarGroupAction.vue +31 -0
- package/src/components/ui/sidebar/SidebarGroupContent.vue +14 -0
- package/src/components/ui/sidebar/SidebarGroupLabel.vue +29 -0
- package/src/components/ui/sidebar/SidebarHeader.vue +14 -0
- package/src/components/ui/sidebar/SidebarInput.vue +15 -0
- package/src/components/ui/sidebar/SidebarInset.vue +22 -0
- package/src/components/ui/sidebar/SidebarMenu.vue +14 -0
- package/src/components/ui/sidebar/SidebarMenuAction.vue +41 -0
- package/src/components/ui/sidebar/SidebarMenuBadge.vue +27 -0
- package/src/components/ui/sidebar/SidebarMenuButton.vue +52 -0
- package/src/components/ui/sidebar/SidebarMenuButtonChild.vue +33 -0
- package/src/components/ui/sidebar/SidebarMenuItem.vue +14 -0
- package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +22 -0
- package/src/components/ui/sidebar/SidebarMenuSub.vue +23 -0
- package/src/components/ui/sidebar/SidebarMenuSubButton.vue +42 -0
- package/src/components/ui/sidebar/SidebarMenuSubItem.vue +7 -0
- package/src/components/ui/sidebar/SidebarProvider.vue +89 -0
- package/src/components/ui/sidebar/SidebarRail.vue +34 -0
- package/src/components/ui/sidebar/SidebarSeparator.vue +15 -0
- package/src/components/ui/sidebar/SidebarTrigger.vue +20 -0
- package/src/components/ui/sidebar/index.ts +51 -0
- package/src/components/ui/sidebar/utils.ts +19 -0
- package/src/components/ui/skeleton/Skeleton.vue +14 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/tooltip/Tooltip.vue +14 -0
- package/src/components/ui/tooltip/TooltipContent.vue +39 -0
- package/src/components/ui/tooltip/TooltipProvider.vue +11 -0
- package/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/composables/useAppearance.ts +53 -0
- package/src/composables/useInitials.ts +14 -0
- package/src/index.ts +22 -0
- package/src/layouts/AdminLayout.vue +170 -0
- package/src/layouts/AuthLayout.vue +14 -0
- package/src/layouts/PublicLayout.vue +53 -0
- package/src/layouts/auth/AuthCardLayout.vue +36 -0
- package/src/layouts/auth/AuthSimpleLayout.vue +31 -0
- package/src/layouts/auth/AuthSplitLayout.vue +40 -0
- package/src/layouts/settings/Layout.vue +56 -0
- package/src/lib/utils.ts +6 -0
- package/src/pages/Admin/Appearance/Theme.vue +58 -0
- package/src/pages/Admin/Appearance/Widgets.vue +48 -0
- package/src/pages/Admin/Commerce/Orders/Index.vue +80 -0
- package/src/pages/Admin/Commerce/Orders/Show.vue +200 -0
- package/src/pages/Admin/Commerce/Products/Edit.vue +167 -0
- package/src/pages/Admin/Commerce/Products/Index.vue +65 -0
- package/src/pages/Admin/Content/Edit.vue +170 -0
- package/src/pages/Admin/Content/Index.vue +88 -0
- package/src/pages/Admin/Content/Preview.vue +25 -0
- package/src/pages/Admin/Dashboard.vue +26 -0
- package/src/pages/Admin/Forms/Edit.vue +98 -0
- package/src/pages/Admin/Forms/Index.vue +68 -0
- package/src/pages/Admin/Forms/Submissions/Index.vue +68 -0
- package/src/pages/Admin/Forms/Submissions/Show.vue +47 -0
- package/src/pages/Admin/Media/Index.vue +75 -0
- package/src/pages/Admin/Menus/Create.vue +37 -0
- package/src/pages/Admin/Menus/Edit.vue +54 -0
- package/src/pages/Admin/Menus/Index.vue +52 -0
- package/src/pages/Admin/Settings/Index.vue +184 -0
- package/src/pages/Admin/Taxonomy/Edit.vue +83 -0
- package/src/pages/Admin/Taxonomy/Index.vue +68 -0
- package/src/pages/Admin/Users/Edit.vue +82 -0
- package/src/pages/Admin/Users/Index.vue +74 -0
- package/src/pages/Public/Cart/Index.vue +108 -0
- package/src/pages/Public/Checkout/Confirmation.vue +110 -0
- package/src/pages/Public/Checkout/Index.vue +174 -0
- package/src/pages/Public/Index.vue +54 -0
- package/src/pages/Public/Shop/Index.vue +39 -0
- package/src/pages/Public/Shop/Show.vue +46 -0
- package/src/pages/Public/Show.vue +41 -0
- package/src/pages/Setup/Complete.vue +53 -0
- package/src/pages/Setup/Index.vue +85 -0
- package/src/pages/Welcome.vue +787 -0
- package/src/pages/auth/ConfirmPassword.vue +53 -0
- package/src/pages/auth/ForgotPassword.vue +54 -0
- package/src/pages/auth/Login.vue +91 -0
- package/src/pages/auth/Register.vue +83 -0
- package/src/pages/auth/ResetPassword.vue +81 -0
- package/src/pages/auth/VerifyEmail.vue +36 -0
- package/src/pages/settings/Appearance.vue +23 -0
- package/src/pages/settings/Password.vue +120 -0
- package/src/pages/settings/Profile.vue +105 -0
- package/src/pages.ts +9 -0
- package/src/types/index.ts +42 -0
- package/src/types/ziggy.ts +12 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { Link, router } from '@inertiajs/vue3';
|
|
4
|
+
import { Button } from '@tower_74/cms-ui';
|
|
5
|
+
|
|
6
|
+
interface FormRow {
|
|
7
|
+
id: number;
|
|
8
|
+
slug: string;
|
|
9
|
+
name: string;
|
|
10
|
+
fields: number;
|
|
11
|
+
submissions: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
defineProps<{ forms: FormRow[]; canManage: boolean }>();
|
|
15
|
+
|
|
16
|
+
const destroy = (form: FormRow) => {
|
|
17
|
+
if (confirm(`Delete the “${form.name}” form and its submissions?`)) {
|
|
18
|
+
router.delete(`/admin/forms/${form.slug}`, { preserveScroll: true });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<AdminLayout>
|
|
25
|
+
<template #title>Forms</template>
|
|
26
|
+
|
|
27
|
+
<div class="mb-4 flex justify-end">
|
|
28
|
+
<Link v-if="canManage" href="/admin/forms/create"><Button>New form</Button></Link>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div v-if="forms.length" class="overflow-hidden rounded-lg border border-border">
|
|
32
|
+
<table class="w-full text-sm">
|
|
33
|
+
<thead class="bg-surface text-left text-muted-foreground">
|
|
34
|
+
<tr>
|
|
35
|
+
<th class="px-4 py-2 font-medium">Form</th>
|
|
36
|
+
<th class="px-4 py-2 font-medium">Fields</th>
|
|
37
|
+
<th class="px-4 py-2 font-medium">Submissions</th>
|
|
38
|
+
<th class="px-4 py-2"></th>
|
|
39
|
+
</tr>
|
|
40
|
+
</thead>
|
|
41
|
+
<tbody>
|
|
42
|
+
<tr v-for="form in forms" :key="form.id" class="border-t border-border">
|
|
43
|
+
<td class="px-4 py-2">
|
|
44
|
+
<span class="font-medium">{{ form.name }}</span>
|
|
45
|
+
<span class="ml-2 text-xs text-muted-foreground">/{{ form.slug }}</span>
|
|
46
|
+
</td>
|
|
47
|
+
<td class="px-4 py-2 text-muted-foreground">{{ form.fields }}</td>
|
|
48
|
+
<td class="px-4 py-2">
|
|
49
|
+
<Link :href="`/admin/forms/${form.slug}/submissions`" class="text-primary hover:underline">{{ form.submissions }}</Link>
|
|
50
|
+
</td>
|
|
51
|
+
<td class="px-4 py-2 text-right">
|
|
52
|
+
<div class="flex justify-end gap-2">
|
|
53
|
+
<Link :href="`/admin/forms/${form.slug}/submissions`"><Button variant="ghost" size="sm">Submissions</Button></Link>
|
|
54
|
+
<template v-if="canManage">
|
|
55
|
+
<Link :href="`/admin/forms/${form.slug}/edit`"><Button variant="ghost" size="sm">Edit</Button></Link>
|
|
56
|
+
<Button variant="danger" size="sm" @click="destroy(form)">Delete</Button>
|
|
57
|
+
</template>
|
|
58
|
+
</div>
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
|
64
|
+
<p v-else class="rounded border border-border bg-background px-4 py-10 text-center text-muted-foreground">
|
|
65
|
+
No forms yet.<template v-if="canManage"> Create one to get started.</template>
|
|
66
|
+
</p>
|
|
67
|
+
</AdminLayout>
|
|
68
|
+
</template>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Pagination from '@/components/Pagination.vue';
|
|
3
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
4
|
+
import { Link, router } from '@inertiajs/vue3';
|
|
5
|
+
import { Button } from '@tower_74/cms-ui';
|
|
6
|
+
|
|
7
|
+
interface Row {
|
|
8
|
+
id: number;
|
|
9
|
+
summary: string;
|
|
10
|
+
received_at: string | null;
|
|
11
|
+
}
|
|
12
|
+
interface PageLink {
|
|
13
|
+
url: string | null;
|
|
14
|
+
label: string;
|
|
15
|
+
active: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
form: { slug: string; name: string };
|
|
20
|
+
submissions: { data: Row[]; links: PageLink[] };
|
|
21
|
+
canDelete: boolean;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const destroy = (id: number) => {
|
|
25
|
+
if (confirm('Delete this submission?')) {
|
|
26
|
+
router.delete(`/admin/forms/${props.form.slug}/submissions/${id}`, { preserveScroll: true });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<AdminLayout>
|
|
33
|
+
<template #title>{{ form.name }} — submissions</template>
|
|
34
|
+
|
|
35
|
+
<Link href="/admin/forms" class="mb-4 inline-block text-sm text-primary hover:underline">← All forms</Link>
|
|
36
|
+
|
|
37
|
+
<div v-if="submissions.data.length" class="overflow-hidden rounded-lg border border-border">
|
|
38
|
+
<table class="w-full text-sm">
|
|
39
|
+
<thead class="bg-surface text-left text-muted-foreground">
|
|
40
|
+
<tr>
|
|
41
|
+
<th class="px-4 py-2 font-medium">From</th>
|
|
42
|
+
<th class="px-4 py-2 font-medium">Received</th>
|
|
43
|
+
<th class="px-4 py-2"></th>
|
|
44
|
+
</tr>
|
|
45
|
+
</thead>
|
|
46
|
+
<tbody>
|
|
47
|
+
<tr v-for="row in submissions.data" :key="row.id" class="border-t border-border">
|
|
48
|
+
<td class="px-4 py-2">
|
|
49
|
+
<Link :href="`/admin/forms/${form.slug}/submissions/${row.id}`" class="font-medium text-primary hover:underline">{{
|
|
50
|
+
row.summary
|
|
51
|
+
}}</Link>
|
|
52
|
+
</td>
|
|
53
|
+
<td class="px-4 py-2 text-muted-foreground">{{ row.received_at }}</td>
|
|
54
|
+
<td class="px-4 py-2 text-right">
|
|
55
|
+
<div class="flex justify-end gap-2">
|
|
56
|
+
<Link :href="`/admin/forms/${form.slug}/submissions/${row.id}`"><Button variant="ghost" size="sm">View</Button></Link>
|
|
57
|
+
<Button v-if="canDelete" variant="danger" size="sm" @click="destroy(row.id)">Delete</Button>
|
|
58
|
+
</div>
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
|
64
|
+
<p v-else class="rounded border border-border bg-background px-4 py-10 text-center text-muted-foreground">No submissions yet.</p>
|
|
65
|
+
|
|
66
|
+
<Pagination :links="submissions.links" />
|
|
67
|
+
</AdminLayout>
|
|
68
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { Link, router } from '@inertiajs/vue3';
|
|
4
|
+
import { Button } from '@tower_74/cms-ui';
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
form: { slug: string; name: string };
|
|
8
|
+
submission: {
|
|
9
|
+
id: number;
|
|
10
|
+
fields: Array<{ label: string; value: string }>;
|
|
11
|
+
ip: string | null;
|
|
12
|
+
received_at: string | null;
|
|
13
|
+
};
|
|
14
|
+
canDelete: boolean;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const destroy = () => {
|
|
18
|
+
if (confirm('Delete this submission?')) {
|
|
19
|
+
router.delete(`/admin/forms/${props.form.slug}/submissions/${props.submission.id}`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<AdminLayout>
|
|
26
|
+
<template #title>{{ form.name }} — submission</template>
|
|
27
|
+
|
|
28
|
+
<div class="max-w-2xl">
|
|
29
|
+
<Link :href="`/admin/forms/${form.slug}/submissions`" class="text-sm text-primary hover:underline">← Back to submissions</Link>
|
|
30
|
+
|
|
31
|
+
<dl class="mt-4 divide-y divide-border rounded-lg border border-border">
|
|
32
|
+
<div v-for="(field, i) in submission.fields" :key="i" class="px-4 py-3">
|
|
33
|
+
<dt class="text-xs font-medium uppercase tracking-wide text-muted-foreground">{{ field.label }}</dt>
|
|
34
|
+
<dd class="mt-1 whitespace-pre-wrap text-sm text-foreground">{{ field.value || '—' }}</dd>
|
|
35
|
+
</div>
|
|
36
|
+
</dl>
|
|
37
|
+
|
|
38
|
+
<p class="mt-3 text-xs text-muted-foreground">
|
|
39
|
+
Received {{ submission.received_at }}<span v-if="submission.ip"> · {{ submission.ip }}</span>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<div v-if="canDelete" class="mt-6">
|
|
43
|
+
<Button variant="danger" @click="destroy">Delete submission</Button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</AdminLayout>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { router, useForm } from '@inertiajs/vue3';
|
|
4
|
+
import { Button } from '@tower_74/cms-ui';
|
|
5
|
+
|
|
6
|
+
interface MediaItem {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
file_name: string;
|
|
10
|
+
mime: string;
|
|
11
|
+
size: string;
|
|
12
|
+
url: string;
|
|
13
|
+
is_image: boolean;
|
|
14
|
+
uploaded_by: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
defineProps<{ items: MediaItem[] }>();
|
|
18
|
+
|
|
19
|
+
const form = useForm<{ file: File | null }>({ file: null });
|
|
20
|
+
|
|
21
|
+
const onFile = (event: Event) => {
|
|
22
|
+
const target = event.target as HTMLInputElement;
|
|
23
|
+
form.file = target.files?.[0] ?? null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const upload = () => {
|
|
27
|
+
form.post('/admin/media', {
|
|
28
|
+
forceFormData: true,
|
|
29
|
+
preserveScroll: true,
|
|
30
|
+
onSuccess: () => form.reset('file'),
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const destroy = (item: MediaItem) => {
|
|
35
|
+
if (confirm(`Delete “${item.name}”?`)) {
|
|
36
|
+
router.delete(`/admin/media/${item.id}`, { preserveScroll: true });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<AdminLayout>
|
|
43
|
+
<template #title>Media</template>
|
|
44
|
+
|
|
45
|
+
<form class="mb-6 flex items-end gap-3" @submit.prevent="upload">
|
|
46
|
+
<div class="space-y-1">
|
|
47
|
+
<label class="block text-sm font-medium text-text">Upload a file</label>
|
|
48
|
+
<input
|
|
49
|
+
type="file"
|
|
50
|
+
class="block text-sm text-muted file:mr-3 file:rounded file:border file:border-border file:bg-surface file:px-3 file:py-1.5 file:text-sm"
|
|
51
|
+
@change="onFile"
|
|
52
|
+
/>
|
|
53
|
+
<p v-if="form.errors.file" class="text-xs text-danger">{{ form.errors.file }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
<Button type="submit" :disabled="!form.file || form.processing">Upload</Button>
|
|
56
|
+
</form>
|
|
57
|
+
|
|
58
|
+
<div v-if="items.length" class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
|
59
|
+
<div v-for="item in items" :key="item.id" class="overflow-hidden rounded border border-border bg-background">
|
|
60
|
+
<div class="flex h-32 items-center justify-center bg-surface">
|
|
61
|
+
<img v-if="item.is_image" :src="item.url" :alt="item.name" class="h-full w-full object-cover" />
|
|
62
|
+
<span v-else class="px-2 text-center text-xs text-muted">{{ item.mime }}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="space-y-1 p-3">
|
|
65
|
+
<p class="truncate text-sm font-medium" :title="item.name">{{ item.name }}</p>
|
|
66
|
+
<p class="text-xs text-muted">{{ item.size }} · {{ item.uploaded_by ?? '—' }}</p>
|
|
67
|
+
<div class="flex justify-end">
|
|
68
|
+
<Button variant="danger" size="sm" @click="destroy(item)">Delete</Button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<p v-else class="rounded border border-border bg-background px-4 py-10 text-center text-muted">No media yet. Upload your first file above.</p>
|
|
74
|
+
</AdminLayout>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { Link, useForm } from '@inertiajs/vue3';
|
|
4
|
+
import { Button, FormBuilder } from '@tower_74/cms-ui';
|
|
5
|
+
import { computed } from 'vue';
|
|
6
|
+
|
|
7
|
+
const fields = [
|
|
8
|
+
{ name: 'name', label: 'Name', required: true, placeholder: 'Primary' },
|
|
9
|
+
{ name: 'location', label: 'Location', help: 'Leave blank to derive from the name (e.g. "footer").' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const form = useForm({ name: '', location: '' });
|
|
13
|
+
|
|
14
|
+
const model = computed({
|
|
15
|
+
get: () => ({ name: form.name, location: form.location }),
|
|
16
|
+
set: (value: Record<string, unknown>) => {
|
|
17
|
+
form.name = (value.name as string) ?? '';
|
|
18
|
+
form.location = (value.location as string) ?? '';
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const submit = () => form.post('/admin/menus');
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<AdminLayout>
|
|
27
|
+
<template #title>New menu</template>
|
|
28
|
+
|
|
29
|
+
<form class="max-w-xl" @submit.prevent="submit">
|
|
30
|
+
<FormBuilder v-model="model" :fields="fields" :errors="form.errors" />
|
|
31
|
+
<div class="mt-6 flex items-center gap-2">
|
|
32
|
+
<Button type="submit" :disabled="form.processing">Create menu</Button>
|
|
33
|
+
<Link href="/admin/menus"><Button variant="ghost" type="button">Cancel</Button></Link>
|
|
34
|
+
</div>
|
|
35
|
+
</form>
|
|
36
|
+
</AdminLayout>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MenuItemsEditor from '@/components/MenuItemsEditor.vue';
|
|
3
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
4
|
+
import { Link, useForm } from '@inertiajs/vue3';
|
|
5
|
+
import { Button, FormField, Input } from '@tower_74/cms-ui';
|
|
6
|
+
import { computed } from 'vue';
|
|
7
|
+
|
|
8
|
+
interface Item {
|
|
9
|
+
label: string;
|
|
10
|
+
url: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
menu: { id: number; name: string; location: string; items: Item[] };
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const form = useForm({
|
|
18
|
+
name: props.menu.name,
|
|
19
|
+
items: props.menu.items as Item[],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const itemsModel = computed({
|
|
23
|
+
get: () => form.items,
|
|
24
|
+
set: (value: Item[]) => {
|
|
25
|
+
form.items = value;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const submit = () => form.put(`/admin/menus/${props.menu.id}`);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<AdminLayout>
|
|
34
|
+
<template #title>Edit menu — {{ menu.name }}</template>
|
|
35
|
+
|
|
36
|
+
<form class="max-w-2xl" @submit.prevent="submit">
|
|
37
|
+
<div class="max-w-sm">
|
|
38
|
+
<FormField label="Menu name" :error="form.errors.name" required>
|
|
39
|
+
<Input v-model="form.name" />
|
|
40
|
+
</FormField>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="mt-8">
|
|
44
|
+
<h3 class="mb-3 text-base font-semibold">Items</h3>
|
|
45
|
+
<MenuItemsEditor v-model="itemsModel" />
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="mt-8 flex items-center gap-2">
|
|
49
|
+
<Button type="submit" :disabled="form.processing">Save menu</Button>
|
|
50
|
+
<Link href="/admin/menus"><Button variant="ghost" type="button">Cancel</Button></Link>
|
|
51
|
+
</div>
|
|
52
|
+
</form>
|
|
53
|
+
</AdminLayout>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { Link, router } from '@inertiajs/vue3';
|
|
4
|
+
import { Button, DataTable } from '@tower_74/cms-ui';
|
|
5
|
+
|
|
6
|
+
interface MenuRow {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
location: string;
|
|
10
|
+
items_count: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
defineProps<{ menus: MenuRow[] }>();
|
|
14
|
+
|
|
15
|
+
const columns = [
|
|
16
|
+
{ key: 'name', label: 'Name' },
|
|
17
|
+
{ key: 'location', label: 'Location' },
|
|
18
|
+
{ key: 'items_count', label: 'Items', align: 'right' as const },
|
|
19
|
+
{ key: 'actions', label: '', align: 'right' as const },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const destroy = (row: MenuRow) => {
|
|
23
|
+
if (confirm(`Delete the “${row.name}” menu?`)) {
|
|
24
|
+
router.delete(`/admin/menus/${row.id}`, { preserveScroll: true });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<AdminLayout>
|
|
31
|
+
<template #title>Menus</template>
|
|
32
|
+
|
|
33
|
+
<div class="mb-4 flex items-center justify-between">
|
|
34
|
+
<h2 class="text-lg font-semibold">Menus</h2>
|
|
35
|
+
<Link href="/admin/menus/create"><Button>New menu</Button></Link>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<DataTable :columns="columns" :rows="menus" empty="No menus yet.">
|
|
39
|
+
<template #cell-location="{ value }">
|
|
40
|
+
<span class="text-muted">{{ value }}</span>
|
|
41
|
+
</template>
|
|
42
|
+
<template #cell-actions="{ row }">
|
|
43
|
+
<div class="flex justify-end gap-2">
|
|
44
|
+
<Link :href="`/admin/menus/${(row as MenuRow).id}/edit`">
|
|
45
|
+
<Button variant="ghost" size="sm">Edit</Button>
|
|
46
|
+
</Link>
|
|
47
|
+
<Button variant="danger" size="sm" @click="destroy(row as MenuRow)">Delete</Button>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
</DataTable>
|
|
51
|
+
</AdminLayout>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { useForm } from '@inertiajs/vue3';
|
|
4
|
+
import { Button, FormBuilder } from '@tower_74/cms-ui';
|
|
5
|
+
import { computed, ref } from 'vue';
|
|
6
|
+
|
|
7
|
+
interface CommerceFeature {
|
|
8
|
+
available: boolean;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
stripeConfigured: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
siteName: string;
|
|
15
|
+
homePageId: number | null;
|
|
16
|
+
ogImage: string | null;
|
|
17
|
+
siteLogo: string | null;
|
|
18
|
+
siteLogoShowName: boolean;
|
|
19
|
+
pages: Array<{ label: string; value: number }>;
|
|
20
|
+
canManageFeatures: boolean;
|
|
21
|
+
features: { commerce?: CommerceFeature; contactForm: { enabled: boolean } };
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const fields = computed(() => [
|
|
25
|
+
{ name: 'site_name', label: 'Site name', required: true },
|
|
26
|
+
{
|
|
27
|
+
name: 'home_page_id',
|
|
28
|
+
label: 'Home page',
|
|
29
|
+
type: 'select' as const,
|
|
30
|
+
help: 'The page shown at the site root (/).',
|
|
31
|
+
options: [{ label: '— Welcome screen —', value: '' }, ...props.pages],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'og_image',
|
|
35
|
+
label: 'Default social image (URL)',
|
|
36
|
+
help: 'Used for Open Graph previews when a page has no image.',
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const form = useForm<{
|
|
41
|
+
site_name: string;
|
|
42
|
+
home_page_id: string;
|
|
43
|
+
og_image: string;
|
|
44
|
+
logo: File | null;
|
|
45
|
+
remove_logo: boolean;
|
|
46
|
+
site_logo_show_name: boolean;
|
|
47
|
+
}>({
|
|
48
|
+
site_name: props.siteName,
|
|
49
|
+
home_page_id: props.homePageId != null ? String(props.homePageId) : '',
|
|
50
|
+
og_image: props.ogImage ?? '',
|
|
51
|
+
logo: null,
|
|
52
|
+
remove_logo: false,
|
|
53
|
+
site_logo_show_name: props.siteLogoShowName,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const model = computed({
|
|
57
|
+
get: () => ({ site_name: form.site_name, home_page_id: form.home_page_id, og_image: form.og_image }),
|
|
58
|
+
set: (value: Record<string, unknown>) => {
|
|
59
|
+
form.site_name = (value.site_name as string) ?? '';
|
|
60
|
+
form.home_page_id = (value.home_page_id as string) ?? '';
|
|
61
|
+
form.og_image = (value.og_image as string) ?? '';
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const newLogoPreview = ref<string | null>(null);
|
|
66
|
+
const logoPreview = computed(() => {
|
|
67
|
+
if (form.remove_logo) return null;
|
|
68
|
+
return newLogoPreview.value ?? props.siteLogo;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const onLogoChange = (event: Event) => {
|
|
72
|
+
const file = (event.target as HTMLInputElement).files?.[0] ?? null;
|
|
73
|
+
form.logo = file;
|
|
74
|
+
form.remove_logo = false;
|
|
75
|
+
newLogoPreview.value = file ? URL.createObjectURL(file) : null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const submit = () =>
|
|
79
|
+
form.put('/admin/settings/general', {
|
|
80
|
+
preserveScroll: true,
|
|
81
|
+
onSuccess: () => {
|
|
82
|
+
form.logo = null;
|
|
83
|
+
newLogoPreview.value = null;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// --- Features -----------------------------------------------------------
|
|
88
|
+
const featureForm = useForm({
|
|
89
|
+
commerce_enabled: props.features.commerce?.enabled ?? false,
|
|
90
|
+
contact_form_enabled: props.features.contactForm.enabled,
|
|
91
|
+
});
|
|
92
|
+
const saveFeatures = () => featureForm.put('/admin/settings/features', { preserveScroll: true });
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<template>
|
|
96
|
+
<AdminLayout>
|
|
97
|
+
<template #title>Settings</template>
|
|
98
|
+
|
|
99
|
+
<div class="max-w-xl space-y-10">
|
|
100
|
+
<!-- General -->
|
|
101
|
+
<section>
|
|
102
|
+
<h2 class="mb-4 text-lg font-semibold">General</h2>
|
|
103
|
+
<form @submit.prevent="submit">
|
|
104
|
+
<FormBuilder v-model="model" :fields="fields" :errors="form.errors" />
|
|
105
|
+
|
|
106
|
+
<div class="mt-6">
|
|
107
|
+
<label class="mb-2 block text-sm font-medium text-foreground">Logo</label>
|
|
108
|
+
<div class="flex items-center gap-4">
|
|
109
|
+
<div
|
|
110
|
+
class="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-md border border-border bg-accent/40"
|
|
111
|
+
>
|
|
112
|
+
<img v-if="logoPreview" :src="logoPreview" alt="Site logo" class="max-h-full max-w-full object-contain" />
|
|
113
|
+
<span v-else class="px-1 text-center text-[10px] leading-tight text-muted-foreground">No logo</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="space-y-2">
|
|
116
|
+
<input
|
|
117
|
+
type="file"
|
|
118
|
+
accept="image/png,image/jpeg,image/svg+xml,image/webp"
|
|
119
|
+
class="block text-sm text-foreground file:mr-3 file:rounded-md file:border-0 file:bg-foreground file:px-3 file:py-1.5 file:text-sm file:text-background hover:file:opacity-90"
|
|
120
|
+
@change="onLogoChange"
|
|
121
|
+
/>
|
|
122
|
+
<label v-if="siteLogo" class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
123
|
+
<input v-model="form.remove_logo" type="checkbox" />
|
|
124
|
+
Remove current logo
|
|
125
|
+
</label>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<p v-if="form.errors.logo" class="mt-2 text-sm text-danger">{{ form.errors.logo }}</p>
|
|
129
|
+
<p class="mt-2 text-xs text-muted-foreground">
|
|
130
|
+
PNG, JPG, SVG or WebP, up to 2 MB. If unset, the site name is shown instead.
|
|
131
|
+
</p>
|
|
132
|
+
<label v-if="logoPreview" class="mt-3 flex items-center gap-2 text-sm text-foreground">
|
|
133
|
+
<input v-model="form.site_logo_show_name" type="checkbox" />
|
|
134
|
+
Show the site name next to the logo
|
|
135
|
+
</label>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="mt-6">
|
|
139
|
+
<Button type="submit" :disabled="form.processing">Save settings</Button>
|
|
140
|
+
</div>
|
|
141
|
+
</form>
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
<!-- Features (agency-only) -->
|
|
145
|
+
<section v-if="canManageFeatures" class="border-t border-border pt-8">
|
|
146
|
+
<h2 class="mb-1 text-lg font-semibold">Features</h2>
|
|
147
|
+
<p class="mb-4 text-sm text-muted-foreground">Turn optional features on or off for this site.</p>
|
|
148
|
+
|
|
149
|
+
<form class="space-y-5" @submit.prevent="saveFeatures">
|
|
150
|
+
<div v-if="features.commerce?.available">
|
|
151
|
+
<label class="flex items-start gap-3">
|
|
152
|
+
<input v-model="featureForm.commerce_enabled" type="checkbox" class="mt-1" />
|
|
153
|
+
<span>
|
|
154
|
+
<span class="block text-sm font-medium text-foreground">Store (commerce)</span>
|
|
155
|
+
<span class="block text-xs text-muted-foreground">Enables the storefront, cart, and checkout.</span>
|
|
156
|
+
</span>
|
|
157
|
+
</label>
|
|
158
|
+
<p
|
|
159
|
+
v-if="featureForm.commerce_enabled && !features.commerce?.stripeConfigured"
|
|
160
|
+
class="border-warning/40 bg-warning/10 ml-7 mt-2 rounded-md border px-3 py-2 text-xs text-foreground"
|
|
161
|
+
>
|
|
162
|
+
No Stripe secret key is configured — the store will be visible but checkout won't complete until
|
|
163
|
+
<code>commerce.payments.stripe.secret</code> is set.
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<label class="flex items-start gap-3">
|
|
168
|
+
<input v-model="featureForm.contact_form_enabled" type="checkbox" class="mt-1" />
|
|
169
|
+
<span>
|
|
170
|
+
<span class="block text-sm font-medium text-foreground">Contact form</span>
|
|
171
|
+
<span class="block text-xs text-muted-foreground"
|
|
172
|
+
>Creates a published <code>/contact</code> page with a contact-form block.</span
|
|
173
|
+
>
|
|
174
|
+
</span>
|
|
175
|
+
</label>
|
|
176
|
+
|
|
177
|
+
<div>
|
|
178
|
+
<Button type="submit" :disabled="featureForm.processing">Save features</Button>
|
|
179
|
+
</div>
|
|
180
|
+
</form>
|
|
181
|
+
</section>
|
|
182
|
+
</div>
|
|
183
|
+
</AdminLayout>
|
|
184
|
+
</template>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { Link, useForm } from '@inertiajs/vue3';
|
|
4
|
+
import { Button, FormBuilder } from '@tower_74/cms-ui';
|
|
5
|
+
import { computed } from 'vue';
|
|
6
|
+
|
|
7
|
+
interface TermData {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
description: string | null;
|
|
12
|
+
parent_id: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
taxonomy: { slug: string; label: string; name: string };
|
|
17
|
+
term: TermData | null;
|
|
18
|
+
parents: Array<{ label: string; value: number }>;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const isEdit = computed(() => props.term !== null);
|
|
22
|
+
|
|
23
|
+
const fields = computed(() => [
|
|
24
|
+
{ name: 'name', label: 'Name', required: true, placeholder: `${props.taxonomy.name} name` },
|
|
25
|
+
{ name: 'slug', label: 'Slug', help: 'Leave blank to auto-generate from the name.' },
|
|
26
|
+
{
|
|
27
|
+
name: 'parent_id',
|
|
28
|
+
label: 'Parent',
|
|
29
|
+
type: 'select' as const,
|
|
30
|
+
options: [{ label: '— None —', value: '' }, ...props.parents],
|
|
31
|
+
},
|
|
32
|
+
{ name: 'description', label: 'Description', type: 'textarea' as const },
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const form = useForm({
|
|
36
|
+
name: props.term?.name ?? '',
|
|
37
|
+
slug: props.term?.slug ?? '',
|
|
38
|
+
parent_id: props.term?.parent_id != null ? String(props.term.parent_id) : '',
|
|
39
|
+
description: props.term?.description ?? '',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const model = computed({
|
|
43
|
+
get: () => ({
|
|
44
|
+
name: form.name,
|
|
45
|
+
slug: form.slug,
|
|
46
|
+
parent_id: form.parent_id,
|
|
47
|
+
description: form.description,
|
|
48
|
+
}),
|
|
49
|
+
set: (value: Record<string, unknown>) => {
|
|
50
|
+
form.name = (value.name as string) ?? '';
|
|
51
|
+
form.slug = (value.slug as string) ?? '';
|
|
52
|
+
form.parent_id = (value.parent_id as string) ?? '';
|
|
53
|
+
form.description = (value.description as string) ?? '';
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const submit = () => {
|
|
58
|
+
if (isEdit.value && props.term) {
|
|
59
|
+
form.put(`/admin/taxonomy/${props.taxonomy.slug}/${props.term.id}`);
|
|
60
|
+
} else {
|
|
61
|
+
form.post(`/admin/taxonomy/${props.taxonomy.slug}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<AdminLayout>
|
|
68
|
+
<template #title> {{ isEdit ? 'Edit' : 'New' }} {{ taxonomy.name.toLowerCase() }} </template>
|
|
69
|
+
|
|
70
|
+
<form class="max-w-2xl" @submit.prevent="submit">
|
|
71
|
+
<FormBuilder v-model="model" :fields="fields" :errors="form.errors" />
|
|
72
|
+
|
|
73
|
+
<div class="mt-6 flex items-center gap-2">
|
|
74
|
+
<Button type="submit" :disabled="form.processing">
|
|
75
|
+
{{ isEdit ? 'Save changes' : `Create ${taxonomy.name.toLowerCase()}` }}
|
|
76
|
+
</Button>
|
|
77
|
+
<Link :href="`/admin/taxonomy/${taxonomy.slug}`">
|
|
78
|
+
<Button variant="ghost" type="button">Cancel</Button>
|
|
79
|
+
</Link>
|
|
80
|
+
</div>
|
|
81
|
+
</form>
|
|
82
|
+
</AdminLayout>
|
|
83
|
+
</template>
|