@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,53 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AuthBar from '@/components/AuthBar.vue';
|
|
3
|
+
import { type Block, BlockRenderer } from '@tower_74/cms-ui';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
site: { name: string; logo?: string | null; showName?: boolean };
|
|
8
|
+
menu: Array<{ label: string; url: string }>;
|
|
9
|
+
footerWidgets: Block[];
|
|
10
|
+
cartCount?: number | null;
|
|
11
|
+
edit?: { url: string; label: string } | null;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const year = computed(() => new Date().getFullYear());
|
|
15
|
+
const showName = computed(() => !props.site.logo || props.site.showName);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div class="flex min-h-screen flex-col bg-background text-text">
|
|
20
|
+
<!-- Authenticated admin bar (WordPress-style): quick link into the dashboard. -->
|
|
21
|
+
<AuthBar primary-label="Dashboard" :primary-href="route('dashboard')" primary-kind="dashboard" :edit="edit" />
|
|
22
|
+
|
|
23
|
+
<header class="border-b border-border">
|
|
24
|
+
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-4 sm:px-6">
|
|
25
|
+
<a href="/" class="flex items-center gap-2 text-lg font-semibold">
|
|
26
|
+
<img v-if="site.logo" :src="site.logo" :alt="site.name" class="h-9 w-auto max-w-[12rem] object-contain" />
|
|
27
|
+
<span v-if="showName">{{ site.name }}</span>
|
|
28
|
+
</a>
|
|
29
|
+
<nav class="flex flex-wrap items-center gap-4 text-sm">
|
|
30
|
+
<a v-for="(item, i) in menu" :key="i" :href="item.url" class="text-muted transition-colors hover:text-text">
|
|
31
|
+
{{ item.label }}
|
|
32
|
+
</a>
|
|
33
|
+
<a
|
|
34
|
+
v-if="cartCount !== null && cartCount !== undefined"
|
|
35
|
+
href="/cart"
|
|
36
|
+
class="font-medium text-text transition-colors hover:text-primary"
|
|
37
|
+
>
|
|
38
|
+
Cart ({{ cartCount }})
|
|
39
|
+
</a>
|
|
40
|
+
</nav>
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<main class="flex-1">
|
|
45
|
+
<slot />
|
|
46
|
+
</main>
|
|
47
|
+
|
|
48
|
+
<footer class="border-t border-border bg-surface">
|
|
49
|
+
<BlockRenderer v-if="footerWidgets.length" :blocks="footerWidgets" />
|
|
50
|
+
<div class="mx-auto max-w-6xl px-4 py-6 text-sm text-muted sm:px-6">© {{ year }} {{ site.name }}</div>
|
|
51
|
+
</footer>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
4
|
+
import { Link } from '@inertiajs/vue3';
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}>();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div class="flex min-h-svh flex-col items-center justify-center gap-6 bg-accent p-6 md:p-10">
|
|
14
|
+
<div class="flex w-full max-w-md flex-col gap-6">
|
|
15
|
+
<Link :href="route('home')" class="flex items-center gap-2 self-center font-medium">
|
|
16
|
+
<div class="flex h-9 w-9 items-center justify-center">
|
|
17
|
+
<AppLogoIcon class="size-9 fill-current text-black dark:text-white" />
|
|
18
|
+
</div>
|
|
19
|
+
</Link>
|
|
20
|
+
|
|
21
|
+
<div class="flex flex-col gap-6">
|
|
22
|
+
<Card class="rounded-xl">
|
|
23
|
+
<CardHeader class="px-10 pb-0 pt-8 text-center">
|
|
24
|
+
<CardTitle class="text-xl">{{ title }}</CardTitle>
|
|
25
|
+
<CardDescription>
|
|
26
|
+
{{ description }}
|
|
27
|
+
</CardDescription>
|
|
28
|
+
</CardHeader>
|
|
29
|
+
<CardContent class="px-10 py-8">
|
|
30
|
+
<slot />
|
|
31
|
+
</CardContent>
|
|
32
|
+
</Card>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
|
3
|
+
import { Link } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div class="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-6 md:p-10">
|
|
13
|
+
<div class="w-full max-w-sm">
|
|
14
|
+
<div class="flex flex-col gap-8">
|
|
15
|
+
<div class="flex flex-col items-center gap-4">
|
|
16
|
+
<Link :href="route('home')" class="flex flex-col items-center gap-2 font-medium">
|
|
17
|
+
<div class="mb-1 flex h-9 w-9 items-center justify-center rounded-md">
|
|
18
|
+
<AppLogoIcon class="size-9 fill-current text-[var(--foreground)] dark:text-white" />
|
|
19
|
+
</div>
|
|
20
|
+
<span class="sr-only">{{ title }}</span>
|
|
21
|
+
</Link>
|
|
22
|
+
<div class="space-y-2 text-center">
|
|
23
|
+
<h1 class="text-xl font-medium">{{ title }}</h1>
|
|
24
|
+
<p class="text-center text-sm text-muted-foreground">{{ description }}</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<slot />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
|
3
|
+
import { Link, usePage } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
const page = usePage();
|
|
6
|
+
const name = page.props.name;
|
|
7
|
+
const quote = page.props.quote;
|
|
8
|
+
|
|
9
|
+
defineProps<{
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}>();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
|
|
17
|
+
<div class="relative hidden h-full flex-col bg-accent p-10 text-white dark:border-r lg:flex">
|
|
18
|
+
<div class="absolute inset-0 bg-zinc-900" />
|
|
19
|
+
<Link :href="route('home')" class="relative z-20 flex items-center text-lg font-medium">
|
|
20
|
+
<AppLogoIcon class="mr-2 size-8 fill-current text-white" />
|
|
21
|
+
{{ name }}
|
|
22
|
+
</Link>
|
|
23
|
+
<div v-if="quote" class="relative z-20 mt-auto">
|
|
24
|
+
<blockquote class="space-y-2">
|
|
25
|
+
<p class="text-lg">“{{ quote.message }}”</p>
|
|
26
|
+
<footer class="text-sm text-neutral-300">{{ quote.author }}</footer>
|
|
27
|
+
</blockquote>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="lg:p-8">
|
|
31
|
+
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
|
32
|
+
<div class="flex flex-col space-y-2 text-center">
|
|
33
|
+
<h1 class="text-xl font-medium tracking-tight" v-if="title">{{ title }}</h1>
|
|
34
|
+
<p class="text-sm text-muted-foreground" v-if="description">{{ description }}</p>
|
|
35
|
+
</div>
|
|
36
|
+
<slot />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Heading from '@/components/Heading.vue';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Separator } from '@/components/ui/separator';
|
|
5
|
+
import { type NavItem } from '@/types';
|
|
6
|
+
import { Link } from '@inertiajs/vue3';
|
|
7
|
+
|
|
8
|
+
const sidebarNavItems: NavItem[] = [
|
|
9
|
+
{
|
|
10
|
+
title: 'Profile',
|
|
11
|
+
href: '/settings/profile',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
title: 'Password',
|
|
15
|
+
href: '/settings/password',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: 'Appearance',
|
|
19
|
+
href: '/settings/appearance',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const currentPath = window.location.pathname;
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div class="px-4 py-6">
|
|
28
|
+
<Heading title="Settings" description="Manage your profile and account settings" />
|
|
29
|
+
|
|
30
|
+
<div class="flex flex-col space-y-8 md:space-y-0 lg:flex-row lg:space-x-12 lg:space-y-0">
|
|
31
|
+
<aside class="w-full max-w-xl lg:w-48">
|
|
32
|
+
<nav class="flex flex-col space-x-0 space-y-1">
|
|
33
|
+
<Button
|
|
34
|
+
v-for="item in sidebarNavItems"
|
|
35
|
+
:key="item.href"
|
|
36
|
+
variant="ghost"
|
|
37
|
+
:class="['w-full justify-start', { 'bg-accent': currentPath === item.href }]"
|
|
38
|
+
as-child
|
|
39
|
+
>
|
|
40
|
+
<Link :href="item.href">
|
|
41
|
+
{{ item.title }}
|
|
42
|
+
</Link>
|
|
43
|
+
</Button>
|
|
44
|
+
</nav>
|
|
45
|
+
</aside>
|
|
46
|
+
|
|
47
|
+
<Separator class="my-6 md:hidden" />
|
|
48
|
+
|
|
49
|
+
<div class="flex-1 md:max-w-2xl">
|
|
50
|
+
<section class="max-w-xl space-y-12">
|
|
51
|
+
<slot />
|
|
52
|
+
</section>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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 } from 'vue';
|
|
6
|
+
|
|
7
|
+
interface ThemeValues {
|
|
8
|
+
primary: string;
|
|
9
|
+
primaryForeground: string;
|
|
10
|
+
surface: string;
|
|
11
|
+
text: string;
|
|
12
|
+
success: string;
|
|
13
|
+
warning: string;
|
|
14
|
+
danger: string;
|
|
15
|
+
info: string;
|
|
16
|
+
radius: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{ theme: ThemeValues }>();
|
|
20
|
+
|
|
21
|
+
const fields = [
|
|
22
|
+
{ name: 'primary', label: 'Primary', type: 'color' as const },
|
|
23
|
+
{ name: 'primaryForeground', label: 'Primary text', type: 'color' as const },
|
|
24
|
+
{ name: 'surface', label: 'Surface', type: 'color' as const },
|
|
25
|
+
{ name: 'text', label: 'Text', type: 'color' as const },
|
|
26
|
+
{ name: 'success', label: 'Success', type: 'color' as const },
|
|
27
|
+
{ name: 'warning', label: 'Warning', type: 'color' as const },
|
|
28
|
+
{ name: 'danger', label: 'Danger', type: 'color' as const },
|
|
29
|
+
{ name: 'info', label: 'Info', type: 'color' as const },
|
|
30
|
+
{ name: 'radius', label: 'Corner radius', help: 'e.g. 0.5rem or 8px' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const form = useForm<ThemeValues>({ ...props.theme });
|
|
34
|
+
|
|
35
|
+
const model = computed({
|
|
36
|
+
get: () => ({ ...form.data() }),
|
|
37
|
+
set: (value: Record<string, unknown>) => {
|
|
38
|
+
Object.assign(form, value);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const submit = () => form.put('/admin/appearance/theme');
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<AdminLayout>
|
|
47
|
+
<template #title>Theme</template>
|
|
48
|
+
|
|
49
|
+
<p class="mb-6 max-w-xl text-sm text-muted">These design tokens re-skin the whole site and component library instantly — no rebuild.</p>
|
|
50
|
+
|
|
51
|
+
<form class="max-w-md" @submit.prevent="submit">
|
|
52
|
+
<FormBuilder v-model="model" :fields="fields" :errors="form.errors" />
|
|
53
|
+
<div class="mt-6">
|
|
54
|
+
<Button type="submit" :disabled="form.processing">Save theme</Button>
|
|
55
|
+
</div>
|
|
56
|
+
</form>
|
|
57
|
+
</AdminLayout>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import BlockEditor from '@/components/BlockEditor.vue';
|
|
3
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
4
|
+
import { useForm } from '@inertiajs/vue3';
|
|
5
|
+
import { Button } from '@tower_74/cms-ui';
|
|
6
|
+
|
|
7
|
+
interface Block {
|
|
8
|
+
type: string;
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
interface BlockDef {
|
|
12
|
+
type: string;
|
|
13
|
+
label: string;
|
|
14
|
+
fields: Array<Record<string, unknown>>;
|
|
15
|
+
defaults: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
areas: Array<{ key: string; label: string }>;
|
|
20
|
+
widgets: Record<string, Block[]>;
|
|
21
|
+
blockRegistry: BlockDef[];
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const form = useForm<{ widgets: Record<string, Block[]> }>({ widgets: { ...props.widgets } });
|
|
25
|
+
|
|
26
|
+
const setArea = (key: string, blocks: Block[]) => {
|
|
27
|
+
form.widgets = { ...form.widgets, [key]: blocks };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const submit = () => form.put('/admin/appearance/widgets');
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<AdminLayout>
|
|
35
|
+
<template #title>Widgets</template>
|
|
36
|
+
|
|
37
|
+
<p class="mb-6 max-w-2xl text-sm text-muted">Widget areas hold blocks rendered in fixed regions of the site (e.g. sidebar, footer).</p>
|
|
38
|
+
|
|
39
|
+
<form class="max-w-3xl space-y-10" @submit.prevent="submit">
|
|
40
|
+
<section v-for="area in areas" :key="area.key">
|
|
41
|
+
<h3 class="mb-3 text-base font-semibold">{{ area.label }}</h3>
|
|
42
|
+
<BlockEditor :model-value="form.widgets[area.key] ?? []" :registry="blockRegistry" @update:model-value="setArea(area.key, $event)" />
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<Button type="submit" :disabled="form.processing">Save widgets</Button>
|
|
46
|
+
</form>
|
|
47
|
+
</AdminLayout>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Pagination from '@/components/Pagination.vue';
|
|
3
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
4
|
+
import { Link } from '@inertiajs/vue3';
|
|
5
|
+
import { Badge, Button, DataTable } from '@tower_74/cms-ui';
|
|
6
|
+
|
|
7
|
+
interface OrderRow {
|
|
8
|
+
id: number;
|
|
9
|
+
number: string;
|
|
10
|
+
date: string | null;
|
|
11
|
+
email: string;
|
|
12
|
+
status: string;
|
|
13
|
+
payment_status: string;
|
|
14
|
+
total: string;
|
|
15
|
+
}
|
|
16
|
+
interface PageLink {
|
|
17
|
+
url: string | null;
|
|
18
|
+
label: string;
|
|
19
|
+
active: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
defineProps<{ orders: { data: OrderRow[]; links: PageLink[] } }>();
|
|
23
|
+
|
|
24
|
+
const columns = [
|
|
25
|
+
{ key: 'number', label: 'Order' },
|
|
26
|
+
{ key: 'date', label: 'Date' },
|
|
27
|
+
{ key: 'email', label: 'Customer' },
|
|
28
|
+
{ key: 'status', label: 'Status' },
|
|
29
|
+
{ key: 'payment_status', label: 'Payment' },
|
|
30
|
+
{ key: 'total', label: 'Total', align: 'right' as const },
|
|
31
|
+
{ key: 'actions', label: '', align: 'right' as const },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const statusVariant = (status: string) =>
|
|
35
|
+
({
|
|
36
|
+
completed: 'success',
|
|
37
|
+
processing: 'info',
|
|
38
|
+
on_hold: 'warning',
|
|
39
|
+
cancelled: 'neutral',
|
|
40
|
+
failed: 'danger',
|
|
41
|
+
refunded: 'neutral',
|
|
42
|
+
pending: 'neutral',
|
|
43
|
+
})[status] ?? 'neutral';
|
|
44
|
+
|
|
45
|
+
const paymentVariant = (status: string) =>
|
|
46
|
+
({
|
|
47
|
+
paid: 'success',
|
|
48
|
+
unpaid: 'neutral',
|
|
49
|
+
authorized: 'info',
|
|
50
|
+
partially_refunded: 'warning',
|
|
51
|
+
refunded: 'neutral',
|
|
52
|
+
failed: 'danger',
|
|
53
|
+
})[status] ?? 'neutral';
|
|
54
|
+
|
|
55
|
+
const label = (value: string) => value.replace(/_/g, ' ');
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<AdminLayout>
|
|
60
|
+
<template #title>Orders</template>
|
|
61
|
+
|
|
62
|
+
<h2 class="mb-4 text-lg font-semibold">Orders</h2>
|
|
63
|
+
|
|
64
|
+
<DataTable :columns="columns" :rows="orders.data" empty="No orders yet.">
|
|
65
|
+
<template #cell-status="{ value }">
|
|
66
|
+
<Badge :variant="statusVariant(value as string)">{{ label(value as string) }}</Badge>
|
|
67
|
+
</template>
|
|
68
|
+
<template #cell-payment_status="{ value }">
|
|
69
|
+
<Badge :variant="paymentVariant(value as string)">{{ label(value as string) }}</Badge>
|
|
70
|
+
</template>
|
|
71
|
+
<template #cell-actions="{ row }">
|
|
72
|
+
<Link :href="`/admin/commerce/orders/${(row as OrderRow).number}`">
|
|
73
|
+
<Button variant="ghost" size="sm">View</Button>
|
|
74
|
+
</Link>
|
|
75
|
+
</template>
|
|
76
|
+
</DataTable>
|
|
77
|
+
|
|
78
|
+
<Pagination :links="orders.links" />
|
|
79
|
+
</AdminLayout>
|
|
80
|
+
</template>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
3
|
+
import { Link, router } from '@inertiajs/vue3';
|
|
4
|
+
import { Badge, Button, Card, Select } from '@tower_74/cms-ui';
|
|
5
|
+
import { ref } from 'vue';
|
|
6
|
+
|
|
7
|
+
interface OrderItem {
|
|
8
|
+
name: string;
|
|
9
|
+
variant: string | null;
|
|
10
|
+
sku: string | null;
|
|
11
|
+
quantity: number;
|
|
12
|
+
unit: string;
|
|
13
|
+
total: string;
|
|
14
|
+
}
|
|
15
|
+
interface Address {
|
|
16
|
+
first_name?: string;
|
|
17
|
+
last_name?: string;
|
|
18
|
+
line1?: string;
|
|
19
|
+
line2?: string;
|
|
20
|
+
city?: string;
|
|
21
|
+
region?: string;
|
|
22
|
+
postal_code?: string;
|
|
23
|
+
country?: string;
|
|
24
|
+
phone?: string;
|
|
25
|
+
}
|
|
26
|
+
interface Order {
|
|
27
|
+
id: number;
|
|
28
|
+
number: string;
|
|
29
|
+
date: string | null;
|
|
30
|
+
email: string;
|
|
31
|
+
status: string;
|
|
32
|
+
paymentStatus: string;
|
|
33
|
+
billing: Address | null;
|
|
34
|
+
shipping: Address | null;
|
|
35
|
+
shippingMethod: string | null;
|
|
36
|
+
note: string | null;
|
|
37
|
+
isPaid: boolean;
|
|
38
|
+
items: OrderItem[];
|
|
39
|
+
totals: { subtotal: string; discount?: string | null; shipping?: string | null; tax?: string | null; total: string };
|
|
40
|
+
payments: Array<{ gateway: string; transactionId: string | null; amount: string; status: string }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const props = defineProps<{
|
|
44
|
+
order: Order;
|
|
45
|
+
fulfillmentStatuses: string[];
|
|
46
|
+
canEdit: boolean;
|
|
47
|
+
canRefund: boolean;
|
|
48
|
+
canDelete: boolean;
|
|
49
|
+
}>();
|
|
50
|
+
|
|
51
|
+
const label = (value: string) => value.replace(/_/g, ' ');
|
|
52
|
+
|
|
53
|
+
const statusVariant = (status: string) =>
|
|
54
|
+
({ completed: 'success', processing: 'info', on_hold: 'warning', failed: 'danger', refunded: 'neutral' })[status] ?? 'neutral';
|
|
55
|
+
|
|
56
|
+
const newStatus = ref(props.order.status);
|
|
57
|
+
|
|
58
|
+
const statusOptions = props.fulfillmentStatuses.map((value) => ({ label: label(value), value }));
|
|
59
|
+
|
|
60
|
+
const updateStatus = () => router.patch(`/admin/commerce/orders/${props.order.number}/status`, { status: newStatus.value }, { preserveScroll: true });
|
|
61
|
+
|
|
62
|
+
const refund = () => {
|
|
63
|
+
if (confirm('Issue a refund for this order? The gateway will confirm it shortly.')) {
|
|
64
|
+
router.post(`/admin/commerce/orders/${props.order.number}/refund`, {}, { preserveScroll: true });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const destroy = () => {
|
|
69
|
+
if (confirm(`Delete order ${props.order.number}? This cannot be undone.`)) {
|
|
70
|
+
router.delete(`/admin/commerce/orders/${props.order.number}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const formatAddress = (a: Address | null) =>
|
|
75
|
+
a
|
|
76
|
+
? [
|
|
77
|
+
`${a.first_name ?? ''} ${a.last_name ?? ''}`.trim(),
|
|
78
|
+
a.line1,
|
|
79
|
+
a.line2,
|
|
80
|
+
[a.city, a.region, a.postal_code].filter(Boolean).join(' '),
|
|
81
|
+
a.country,
|
|
82
|
+
a.phone,
|
|
83
|
+
].filter(Boolean)
|
|
84
|
+
: [];
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<AdminLayout>
|
|
89
|
+
<template #title>Order {{ order.number }}</template>
|
|
90
|
+
|
|
91
|
+
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
|
92
|
+
<div class="flex items-center gap-3">
|
|
93
|
+
<Link href="/admin/commerce/orders" class="text-sm text-muted hover:underline">← Orders</Link>
|
|
94
|
+
<h2 class="text-lg font-semibold">{{ order.number }}</h2>
|
|
95
|
+
<Badge :variant="statusVariant(order.status)">{{ label(order.status) }}</Badge>
|
|
96
|
+
<Badge :variant="order.paymentStatus === 'paid' ? 'success' : 'neutral'">{{ label(order.paymentStatus) }}</Badge>
|
|
97
|
+
</div>
|
|
98
|
+
<a :href="`/admin/commerce/orders/${order.number}/invoice`">
|
|
99
|
+
<Button variant="secondary" size="sm">Download invoice</Button>
|
|
100
|
+
</a>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="grid gap-6 lg:grid-cols-[1fr_320px]">
|
|
104
|
+
<div class="space-y-6">
|
|
105
|
+
<Card title="Items">
|
|
106
|
+
<table class="w-full text-sm">
|
|
107
|
+
<thead>
|
|
108
|
+
<tr class="border-b border-border text-left text-muted">
|
|
109
|
+
<th class="py-2">Item</th>
|
|
110
|
+
<th class="py-2 text-right">Qty</th>
|
|
111
|
+
<th class="py-2 text-right">Unit</th>
|
|
112
|
+
<th class="py-2 text-right">Total</th>
|
|
113
|
+
</tr>
|
|
114
|
+
</thead>
|
|
115
|
+
<tbody>
|
|
116
|
+
<tr v-for="(item, i) in order.items" :key="i" class="border-b border-border last:border-0">
|
|
117
|
+
<td class="py-2">
|
|
118
|
+
{{ item.name }}
|
|
119
|
+
<span v-if="item.variant" class="block text-xs text-muted">{{ item.variant }}</span>
|
|
120
|
+
</td>
|
|
121
|
+
<td class="py-2 text-right">{{ item.quantity }}</td>
|
|
122
|
+
<td class="py-2 text-right">{{ item.unit }}</td>
|
|
123
|
+
<td class="py-2 text-right">{{ item.total }}</td>
|
|
124
|
+
</tr>
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
|
|
128
|
+
<dl class="mt-4 space-y-1 border-t border-border pt-4 text-sm">
|
|
129
|
+
<div class="flex justify-between">
|
|
130
|
+
<dt class="text-muted">Subtotal</dt>
|
|
131
|
+
<dd>{{ order.totals.subtotal }}</dd>
|
|
132
|
+
</div>
|
|
133
|
+
<div v-if="order.totals.discount" class="flex justify-between">
|
|
134
|
+
<dt class="text-muted">Discount</dt>
|
|
135
|
+
<dd>-{{ order.totals.discount }}</dd>
|
|
136
|
+
</div>
|
|
137
|
+
<div v-if="order.totals.shipping" class="flex justify-between">
|
|
138
|
+
<dt class="text-muted">Shipping</dt>
|
|
139
|
+
<dd>{{ order.totals.shipping }}</dd>
|
|
140
|
+
</div>
|
|
141
|
+
<div v-if="order.totals.tax" class="flex justify-between">
|
|
142
|
+
<dt class="text-muted">Tax</dt>
|
|
143
|
+
<dd>{{ order.totals.tax }}</dd>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="flex justify-between border-t border-border pt-2 font-semibold">
|
|
146
|
+
<dt>Total</dt>
|
|
147
|
+
<dd>{{ order.totals.total }}</dd>
|
|
148
|
+
</div>
|
|
149
|
+
</dl>
|
|
150
|
+
</Card>
|
|
151
|
+
|
|
152
|
+
<Card title="Payments">
|
|
153
|
+
<p v-if="!order.payments.length" class="text-sm text-muted">No payments recorded.</p>
|
|
154
|
+
<ul v-else class="space-y-2 text-sm">
|
|
155
|
+
<li v-for="(p, i) in order.payments" :key="i" class="flex items-center justify-between">
|
|
156
|
+
<span>
|
|
157
|
+
<span class="font-medium">{{ p.amount }}</span>
|
|
158
|
+
<span class="text-muted"> · {{ p.gateway }} · {{ label(p.status) }}</span>
|
|
159
|
+
</span>
|
|
160
|
+
<span class="font-mono text-xs text-muted">{{ p.transactionId }}</span>
|
|
161
|
+
</li>
|
|
162
|
+
</ul>
|
|
163
|
+
</Card>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="space-y-6">
|
|
167
|
+
<Card title="Customer">
|
|
168
|
+
<p class="text-sm">{{ order.email }}</p>
|
|
169
|
+
<p class="mt-1 text-xs text-muted">Placed {{ order.date }}</p>
|
|
170
|
+
</Card>
|
|
171
|
+
|
|
172
|
+
<Card title="Shipping address">
|
|
173
|
+
<p v-for="(line, i) in formatAddress(order.shipping)" :key="i" class="text-sm">{{ line }}</p>
|
|
174
|
+
<p v-if="order.shippingMethod" class="mt-2 text-xs text-muted">Method: {{ order.shippingMethod }}</p>
|
|
175
|
+
</Card>
|
|
176
|
+
|
|
177
|
+
<Card title="Billing address">
|
|
178
|
+
<p v-for="(line, i) in formatAddress(order.billing)" :key="i" class="text-sm">{{ line }}</p>
|
|
179
|
+
</Card>
|
|
180
|
+
|
|
181
|
+
<Card v-if="order.note" title="Customer note">
|
|
182
|
+
<p class="text-sm">{{ order.note }}</p>
|
|
183
|
+
</Card>
|
|
184
|
+
|
|
185
|
+
<Card title="Manage">
|
|
186
|
+
<div v-if="canEdit" class="space-y-2">
|
|
187
|
+
<label class="text-xs font-medium text-muted">Fulfillment status</label>
|
|
188
|
+
<Select v-model="newStatus" :options="statusOptions" />
|
|
189
|
+
<Button size="sm" class="w-full" @click="updateStatus">Update status</Button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="mt-4 space-y-2 border-t border-border pt-4">
|
|
193
|
+
<Button v-if="canRefund && order.isPaid" variant="secondary" size="sm" class="w-full" @click="refund"> Refund order </Button>
|
|
194
|
+
<Button v-if="canDelete" variant="danger" size="sm" class="w-full" @click="destroy"> Delete order </Button>
|
|
195
|
+
</div>
|
|
196
|
+
</Card>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</AdminLayout>
|
|
200
|
+
</template>
|