@stacksjs/defaults 0.70.362 → 0.70.365
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/ai/skills/stacks-browse/SKILL.md +2 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +20 -2
- package/ai/skills/stacks-dashboard/SKILL.md +4 -0
- package/app/Actions/Auth/LogoutAction.ts +11 -4
- package/app/Actions/Auth/LogoutAllAction.ts +8 -4
- package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
- package/app/Actions/Dashboard/Models/model-write.ts +5 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Buttons/Counter.stx +1 -1
- package/resources/components/Buttons/ToggleDark.stx +1 -1
- package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
- package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
- package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
- package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
- package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
- package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
- package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
- package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
- package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
- package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
- package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
- package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
- package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
- package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
- package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
- package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
- package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
- package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
- package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
- package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
- package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
- package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
- package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
- package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
- package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
- package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
- package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
- package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
- package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
- package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
- package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
- package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
- package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
- package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
- package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
- package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
- package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
- package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
- package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
- package/resources/components/Dashboard/GlobalSearch.stx +3 -9
- package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
- package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
- package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
- package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
- package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
- package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
- package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
- package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
- package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
- package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
- package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
- package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
- package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
- package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
- package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
- package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
- package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
- package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
- package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
- package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
- package/resources/components/Dashboard/Navbar.stx +5 -5
- package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
- package/resources/components/Dashboard/UI/Button.stx +4 -0
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +4 -0
- package/resources/components/Dashboard/UI/Drawer.stx +114 -0
- package/resources/components/Dashboard/UI/Modal.stx +19 -12
- package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
- package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
- package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
- package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
- package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
- package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
- package/resources/components/Docs/TeamMember.stx +16 -9
- package/resources/components/Forum/ForumReplyForm.stx +7 -7
- package/resources/components/Forum/ForumSidebar.stx +10 -6
- package/resources/components/Marketing/Faqs.stx +5 -5
- package/resources/components/Marketing/Feature.stx +11 -6
- package/resources/components/Marketing/Layout.stx +12 -8
- package/resources/components/Marketing/Pricing.stx +11 -5
- package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
- package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
- package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
- package/resources/components/Marketing/Testimonials.stx +8 -5
- package/resources/views/index.stx +4 -4
- package/views/system-tray/index.stx +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
interface PageRecord {
|
|
3
|
+
id: string
|
|
4
|
+
title: string
|
|
5
|
+
template: string
|
|
6
|
+
publishedAt: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface PageDraft {
|
|
10
|
+
title: string
|
|
11
|
+
template: string
|
|
12
|
+
published: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const open = useReactiveProp('open', false)
|
|
16
|
+
const page = useReactiveProp('page', null as PageRecord | null)
|
|
17
|
+
const busy = useReactiveProp('busy', false)
|
|
18
|
+
const error = useReactiveProp('error', '')
|
|
19
|
+
const title = state('')
|
|
20
|
+
const template = state('default')
|
|
21
|
+
const published = state(false)
|
|
22
|
+
const emit = defineEmits()
|
|
23
|
+
let wasOpen = false
|
|
24
|
+
|
|
25
|
+
effect(() => {
|
|
26
|
+
const isOpen = open()
|
|
27
|
+
if (isOpen && !wasOpen) {
|
|
28
|
+
const record = page()
|
|
29
|
+
title.set(record?.title || '')
|
|
30
|
+
template.set(record?.template || 'default')
|
|
31
|
+
published.set(Boolean(record?.publishedAt))
|
|
32
|
+
}
|
|
33
|
+
wasOpen = isOpen
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function close(): void {
|
|
37
|
+
if (!busy())
|
|
38
|
+
emit('close')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function submit(): void {
|
|
42
|
+
const draft: PageDraft = {
|
|
43
|
+
title: title().trim(),
|
|
44
|
+
template: template(),
|
|
45
|
+
published: published(),
|
|
46
|
+
}
|
|
47
|
+
emit('submit', draft)
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<Modal
|
|
52
|
+
:isOpen="open()"
|
|
53
|
+
:title="page() ? 'Edit page' : 'New page'"
|
|
54
|
+
description="Configure the page template and publishing state."
|
|
55
|
+
:closable="!busy()"
|
|
56
|
+
@close="close"
|
|
57
|
+
>
|
|
58
|
+
<form id="page-editor-form" class="grid gap-4" @submit.prevent="submit">
|
|
59
|
+
<label>
|
|
60
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Title</span>
|
|
61
|
+
<input x-model="title" name="title" required minlength="3" maxlength="255" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" />
|
|
62
|
+
</label>
|
|
63
|
+
<label>
|
|
64
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Template</span>
|
|
65
|
+
<select x-model="template" name="template" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700">
|
|
66
|
+
<option value="default">Default</option>
|
|
67
|
+
<option value="landing">Landing</option>
|
|
68
|
+
<option value="blog">Blog</option>
|
|
69
|
+
<option value="contact">Contact</option>
|
|
70
|
+
</select>
|
|
71
|
+
</label>
|
|
72
|
+
<label class="flex gap-3 items-start p-3 bg-gray-50 dark:bg-neutral-900/50 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
73
|
+
<input x-model="published" name="published" type="checkbox" class="mt-0.5 h-4 w-4 rounded" />
|
|
74
|
+
<span>
|
|
75
|
+
<span class="block font-medium text-gray-700 text-sm dark:text-neutral-200">Published</span>
|
|
76
|
+
<span class="block mt-1 text-gray-500 text-xs dark:text-neutral-400">Published pages receive a timestamp. Clearing this returns the page to draft.</span>
|
|
77
|
+
</span>
|
|
78
|
+
</label>
|
|
79
|
+
</form>
|
|
80
|
+
|
|
81
|
+
<div :if="error()" role="alert" class="mt-4 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">
|
|
82
|
+
{{ error() }}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<template #footer>
|
|
86
|
+
<div class="flex gap-2 justify-end w-full">
|
|
87
|
+
<Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
|
|
88
|
+
<Button type="submit" form="page-editor-form" :disabled="title().trim().length < 3" :loading="busy()">
|
|
89
|
+
{{ page() ? 'Save changes' : 'Create page' }}
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
</Modal>
|
|
@@ -11,15 +11,18 @@ interface PageRecord {
|
|
|
11
11
|
publishedAt: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
interface PageDraft {
|
|
15
|
+
title: string
|
|
16
|
+
template: string
|
|
17
|
+
published: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
const pages = state<PageRecord[]>([])
|
|
15
21
|
const loading = state(true)
|
|
16
22
|
const loadError = state('')
|
|
17
23
|
const search = state('')
|
|
18
24
|
const formOpen = state(false)
|
|
19
25
|
const editingPage = state<PageRecord | null>(null)
|
|
20
|
-
const title = state('')
|
|
21
|
-
const template = state('default')
|
|
22
|
-
const published = state(false)
|
|
23
26
|
const saving = state(false)
|
|
24
27
|
const saveError = state('')
|
|
25
28
|
const deletingPage = state<PageRecord | null>(null)
|
|
@@ -82,21 +85,15 @@ async function loadPages(): Promise<void> {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
function resetForm(page: PageRecord | null): void {
|
|
86
|
-
editingPage.set(page)
|
|
87
|
-
title.set(page?.title || '')
|
|
88
|
-
template.set(page?.template || 'default')
|
|
89
|
-
published.set(Boolean(page?.publishedAt))
|
|
90
|
-
saveError.set('')
|
|
91
|
-
}
|
|
92
|
-
|
|
93
88
|
function openCreate(): void {
|
|
94
|
-
|
|
89
|
+
editingPage.set(null)
|
|
90
|
+
saveError.set('')
|
|
95
91
|
formOpen.set(true)
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
function openEdit(page: PageRecord): void {
|
|
99
|
-
|
|
95
|
+
editingPage.set(page)
|
|
96
|
+
saveError.set('')
|
|
100
97
|
formOpen.set(true)
|
|
101
98
|
}
|
|
102
99
|
|
|
@@ -105,14 +102,14 @@ function closeForm(): void {
|
|
|
105
102
|
formOpen.set(false)
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
async function savePage(): Promise<void> {
|
|
105
|
+
async function savePage(draft: PageDraft): Promise<void> {
|
|
109
106
|
const page = editingPage()
|
|
110
107
|
saving.set(true)
|
|
111
108
|
saveError.set('')
|
|
112
109
|
try {
|
|
113
110
|
await dashboardApi(page ? `/api/dashboard/pages/${page.id}` : '/api/dashboard/pages', {
|
|
114
111
|
method: page ? 'PATCH' : 'POST',
|
|
115
|
-
body:
|
|
112
|
+
body: draft,
|
|
116
113
|
})
|
|
117
114
|
formOpen.set(false)
|
|
118
115
|
pushToast('success', page ? 'Page updated' : 'Page created')
|
|
@@ -136,6 +133,11 @@ function closeDelete(): void {
|
|
|
136
133
|
deletingPage.set(null)
|
|
137
134
|
}
|
|
138
135
|
|
|
136
|
+
function deleteDescription(): string {
|
|
137
|
+
const page = deletingPage()
|
|
138
|
+
return page ? `${page.title} will be permanently removed.` : ''
|
|
139
|
+
}
|
|
140
|
+
|
|
139
141
|
async function deletePage(): Promise<void> {
|
|
140
142
|
const page = deletingPage()
|
|
141
143
|
if (!page)
|
|
@@ -186,14 +188,22 @@ onMount(() => {
|
|
|
186
188
|
</section>
|
|
187
189
|
</div>
|
|
188
190
|
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
191
|
+
<PageEditorDialog
|
|
192
|
+
:open="formOpen()"
|
|
193
|
+
:page="editingPage()"
|
|
194
|
+
:busy="saving()"
|
|
195
|
+
:error="saveError()"
|
|
196
|
+
@close="closeForm"
|
|
197
|
+
@submit="savePage"
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<ConfirmDialog
|
|
201
|
+
:isOpen="Boolean(deletingPage())"
|
|
202
|
+
title="Delete page?"
|
|
203
|
+
:description="deleteDescription()"
|
|
204
|
+
confirmLabel="Delete page"
|
|
205
|
+
:busy="deleting()"
|
|
206
|
+
:error="deleteError()"
|
|
207
|
+
@close="closeDelete"
|
|
208
|
+
@confirm="deletePage"
|
|
209
|
+
/>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
type PostStatus = 'draft' | 'published' | 'archived'
|
|
3
|
+
interface OptionRecord { id: string, name: string, slug: string }
|
|
4
|
+
interface AuthorRecord { id: string, name: string, email: string }
|
|
5
|
+
interface PostRecord {
|
|
6
|
+
id: string
|
|
7
|
+
title: string
|
|
8
|
+
excerpt: string
|
|
9
|
+
content: string
|
|
10
|
+
poster: string
|
|
11
|
+
status: PostStatus
|
|
12
|
+
authorId: string
|
|
13
|
+
featured: boolean
|
|
14
|
+
categoryIds: string[]
|
|
15
|
+
tagIds: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PostDraft {
|
|
19
|
+
title: string
|
|
20
|
+
excerpt: string
|
|
21
|
+
content: string
|
|
22
|
+
poster: string
|
|
23
|
+
status: PostStatus
|
|
24
|
+
authorId: string | null
|
|
25
|
+
featured: boolean
|
|
26
|
+
categoryIds: string[]
|
|
27
|
+
tagIds: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const open = useReactiveProp('open', false)
|
|
31
|
+
const post = useReactiveProp('post', null as PostRecord | null)
|
|
32
|
+
const categories = useReactiveProp('categories', [] as OptionRecord[])
|
|
33
|
+
const tags = useReactiveProp('tags', [] as OptionRecord[])
|
|
34
|
+
const authors = useReactiveProp('authors', [] as AuthorRecord[])
|
|
35
|
+
const busy = useReactiveProp('busy', false)
|
|
36
|
+
const error = useReactiveProp('error', '')
|
|
37
|
+
const title = state('')
|
|
38
|
+
const excerpt = state('')
|
|
39
|
+
const content = state('')
|
|
40
|
+
const poster = state('')
|
|
41
|
+
const status = state<PostStatus>('draft')
|
|
42
|
+
const authorId = state('')
|
|
43
|
+
const featured = state(false)
|
|
44
|
+
const categoryIds = state<string[]>([])
|
|
45
|
+
const tagIds = state<string[]>([])
|
|
46
|
+
const emit = defineEmits()
|
|
47
|
+
let wasOpen = false
|
|
48
|
+
|
|
49
|
+
effect(() => {
|
|
50
|
+
const isOpen = open()
|
|
51
|
+
if (isOpen && !wasOpen) {
|
|
52
|
+
const record = post()
|
|
53
|
+
title.set(record?.title || '')
|
|
54
|
+
excerpt.set(record?.excerpt || '')
|
|
55
|
+
content.set(record?.content || '')
|
|
56
|
+
poster.set(record?.poster || '')
|
|
57
|
+
status.set(record?.status || 'draft')
|
|
58
|
+
authorId.set(record?.authorId || '')
|
|
59
|
+
featured.set(Boolean(record?.featured))
|
|
60
|
+
categoryIds.set(record ? [...record.categoryIds] : [])
|
|
61
|
+
tagIds.set(record ? [...record.tagIds] : [])
|
|
62
|
+
}
|
|
63
|
+
wasOpen = isOpen
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
function valid(): boolean {
|
|
67
|
+
const excerptLength = excerpt().trim().length
|
|
68
|
+
return title().trim().length >= 3 && title().trim().length <= 255
|
|
69
|
+
&& content().trim().length >= 10 && content().length <= 100000
|
|
70
|
+
&& (excerptLength === 0 || excerptLength >= 10) && excerpt().length <= 500
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function close(): void {
|
|
74
|
+
if (!busy())
|
|
75
|
+
emit('close')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function submit(): void {
|
|
79
|
+
const draft: PostDraft = {
|
|
80
|
+
title: title().trim(),
|
|
81
|
+
excerpt: excerpt().trim(),
|
|
82
|
+
content: content().trim(),
|
|
83
|
+
poster: poster().trim(),
|
|
84
|
+
status: status(),
|
|
85
|
+
authorId: authorId() || null,
|
|
86
|
+
featured: featured(),
|
|
87
|
+
categoryIds: [...categoryIds()],
|
|
88
|
+
tagIds: [...tagIds()],
|
|
89
|
+
}
|
|
90
|
+
emit('submit', draft)
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<Modal
|
|
95
|
+
:isOpen="open()"
|
|
96
|
+
:title="post() ? 'Edit post' : 'New post'"
|
|
97
|
+
description="Content, ownership, and taxonomies are persisted together."
|
|
98
|
+
size="xl"
|
|
99
|
+
:closable="!busy()"
|
|
100
|
+
@close="close"
|
|
101
|
+
>
|
|
102
|
+
<form id="post-editor-form" class="grid gap-5" @submit.prevent="submit">
|
|
103
|
+
<label>
|
|
104
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Title</span>
|
|
105
|
+
<input x-model.trim="title" name="title" required minlength="3" maxlength="255" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" />
|
|
106
|
+
</label>
|
|
107
|
+
<div class="grid gap-4 sm:grid-cols-3">
|
|
108
|
+
<label>
|
|
109
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Status</span>
|
|
110
|
+
<select x-model="status" name="status" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700">
|
|
111
|
+
<option value="draft">Draft</option>
|
|
112
|
+
<option value="published">Published</option>
|
|
113
|
+
<option value="archived">Archived</option>
|
|
114
|
+
</select>
|
|
115
|
+
</label>
|
|
116
|
+
<label>
|
|
117
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Author</span>
|
|
118
|
+
<select x-model="authorId" name="author_id" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700">
|
|
119
|
+
<option value="">No author</option>
|
|
120
|
+
<template :for="author in authors()"><option :value="author.id">{{ author.name }} ({{ author.email }})</option></template>
|
|
121
|
+
</select>
|
|
122
|
+
</label>
|
|
123
|
+
<label class="flex gap-3 items-center self-end px-3 py-2 min-h-10 bg-gray-50 dark:bg-neutral-900/60 border border-gray-200 rounded-md dark:border-neutral-700">
|
|
124
|
+
<input x-model="featured" name="featured" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded" />
|
|
125
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Featured post</span>
|
|
126
|
+
</label>
|
|
127
|
+
</div>
|
|
128
|
+
<label>
|
|
129
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Poster URL</span>
|
|
130
|
+
<input x-model.trim="poster" name="poster" type="url" inputmode="url" maxlength="255" placeholder="https://example.com/poster.jpg" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" />
|
|
131
|
+
</label>
|
|
132
|
+
<label>
|
|
133
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Excerpt</span>
|
|
134
|
+
<textarea x-model="excerpt" name="excerpt" rows="3" maxlength="500" placeholder="Optional, at least 10 characters when provided" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700"></textarea>
|
|
135
|
+
<span class="block mt-1 text-gray-400 text-xs">{{ excerpt().length }}/500</span>
|
|
136
|
+
</label>
|
|
137
|
+
<label>
|
|
138
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Content</span>
|
|
139
|
+
<textarea x-model="content" name="content" required minlength="10" maxlength="100000" rows="10" class="mt-1 px-3 py-2 w-full font-mono text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700"></textarea>
|
|
140
|
+
<span class="block mt-1 text-gray-400 text-xs">{{ content().length }}/100000</span>
|
|
141
|
+
</label>
|
|
142
|
+
<div class="grid gap-4 lg:grid-cols-2">
|
|
143
|
+
<fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
144
|
+
<legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Categories, {{ categoryIds().length }} selected</legend>
|
|
145
|
+
<div :if="categories().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No categories available.</div>
|
|
146
|
+
<div :else class="grid gap-2">
|
|
147
|
+
<template :for="categoryOption in categories()">
|
|
148
|
+
<label class="flex gap-2 items-center"><input x-model="categoryIds" name="category_ids" type="checkbox" :value="categoryOption.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ categoryOption.name }}</span></label>
|
|
149
|
+
</template>
|
|
150
|
+
</div>
|
|
151
|
+
</fieldset>
|
|
152
|
+
<fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
153
|
+
<legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Tags, {{ tagIds().length }} selected</legend>
|
|
154
|
+
<div :if="tags().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No tags available.</div>
|
|
155
|
+
<div :else class="grid gap-2 sm:grid-cols-2">
|
|
156
|
+
<template :for="tag in tags()">
|
|
157
|
+
<label class="flex gap-2 items-center"><input x-model="tagIds" name="tag_ids" type="checkbox" :value="tag.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ tag.name }}</span></label>
|
|
158
|
+
</template>
|
|
159
|
+
</div>
|
|
160
|
+
</fieldset>
|
|
161
|
+
</div>
|
|
162
|
+
</form>
|
|
163
|
+
|
|
164
|
+
<div :if="error()" role="alert" class="mt-4 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">
|
|
165
|
+
{{ error() }}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<template #footer>
|
|
169
|
+
<div class="flex gap-2 justify-end w-full">
|
|
170
|
+
<Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
|
|
171
|
+
<Button type="submit" form="post-editor-form" :disabled="!valid()" :loading="busy()">
|
|
172
|
+
{{ post() ? 'Save changes' : 'Create post' }}
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</template>
|
|
176
|
+
</Modal>
|
|
@@ -25,6 +25,17 @@ interface PostRecord {
|
|
|
25
25
|
categoryIds: string[]
|
|
26
26
|
tagIds: string[]
|
|
27
27
|
}
|
|
28
|
+
interface PostDraft {
|
|
29
|
+
title: string
|
|
30
|
+
excerpt: string
|
|
31
|
+
content: string
|
|
32
|
+
poster: string
|
|
33
|
+
status: PostStatus
|
|
34
|
+
authorId: string | null
|
|
35
|
+
featured: boolean
|
|
36
|
+
categoryIds: string[]
|
|
37
|
+
tagIds: string[]
|
|
38
|
+
}
|
|
28
39
|
|
|
29
40
|
const posts = state<PostRecord[]>([])
|
|
30
41
|
const categories = state<OptionRecord[]>([])
|
|
@@ -37,15 +48,6 @@ const statusFilter = state('all')
|
|
|
37
48
|
|
|
38
49
|
const formOpen = state(false)
|
|
39
50
|
const editingPost = state<PostRecord | null>(null)
|
|
40
|
-
const title = state('')
|
|
41
|
-
const excerpt = state('')
|
|
42
|
-
const content = state('')
|
|
43
|
-
const poster = state('')
|
|
44
|
-
const status = state<PostStatus>('draft')
|
|
45
|
-
const authorId = state('')
|
|
46
|
-
const featured = state(false)
|
|
47
|
-
const categoryIds = state<string[]>([])
|
|
48
|
-
const tagIds = state<string[]>([])
|
|
49
51
|
const saving = state(false)
|
|
50
52
|
const saveError = state('')
|
|
51
53
|
const deletingPost = state<PostRecord | null>(null)
|
|
@@ -151,51 +153,25 @@ async function loadPosts(): Promise<void> {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
title.set(post?.title || '')
|
|
157
|
-
excerpt.set(post?.excerpt || '')
|
|
158
|
-
content.set(post?.content || '')
|
|
159
|
-
poster.set(post?.poster || '')
|
|
160
|
-
status.set(post?.status || 'draft')
|
|
161
|
-
authorId.set(post?.authorId || '')
|
|
162
|
-
featured.set(Boolean(post?.featured))
|
|
163
|
-
categoryIds.set(post ? [...post.categoryIds] : [])
|
|
164
|
-
tagIds.set(post ? [...post.tagIds] : [])
|
|
165
|
-
saveError.set('')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function openCreate(): void { resetForm(null); formOpen.set(true) }
|
|
169
|
-
function openEdit(post: PostRecord): void { resetForm(post); formOpen.set(true) }
|
|
156
|
+
function openCreate(): void { editingPost.set(null); saveError.set(''); formOpen.set(true) }
|
|
157
|
+
function openEdit(post: PostRecord): void { editingPost.set(post); saveError.set(''); formOpen.set(true) }
|
|
170
158
|
function closeForm(): void { if (!saving()) formOpen.set(false) }
|
|
171
159
|
function openDelete(post: PostRecord): void { deletingPost.set(post); deleteError.set('') }
|
|
172
160
|
function closeDelete(): void { if (!deleting()) deletingPost.set(null) }
|
|
173
161
|
|
|
174
|
-
function
|
|
175
|
-
const
|
|
176
|
-
return title
|
|
177
|
-
&& content().trim().length >= 10 && content().length <= 100000
|
|
178
|
-
&& (excerptLength === 0 || excerptLength >= 10) && excerpt().length <= 500
|
|
162
|
+
function deleteDescription(): string {
|
|
163
|
+
const post = deletingPost()
|
|
164
|
+
return post ? `${post.title} and its category and tag links will be permanently removed.` : ''
|
|
179
165
|
}
|
|
180
166
|
|
|
181
|
-
async function savePost(): Promise<void> {
|
|
167
|
+
async function savePost(draft: PostDraft): Promise<void> {
|
|
182
168
|
const post = editingPost()
|
|
183
169
|
saving.set(true)
|
|
184
170
|
saveError.set('')
|
|
185
171
|
try {
|
|
186
172
|
await dashboardApi(post ? `/api/dashboard/posts/${post.id}` : '/api/dashboard/posts', {
|
|
187
173
|
method: post ? 'PATCH' : 'POST',
|
|
188
|
-
body:
|
|
189
|
-
title: title().trim(),
|
|
190
|
-
excerpt: excerpt().trim(),
|
|
191
|
-
content: content().trim(),
|
|
192
|
-
poster: poster().trim(),
|
|
193
|
-
status: status(),
|
|
194
|
-
authorId: authorId() || null,
|
|
195
|
-
featured: featured(),
|
|
196
|
-
categoryIds: categoryIds(),
|
|
197
|
-
tagIds: tagIds(),
|
|
198
|
-
},
|
|
174
|
+
body: draft,
|
|
199
175
|
})
|
|
200
176
|
formOpen.set(false)
|
|
201
177
|
pushToast('success', post ? 'Post updated' : 'Post created')
|
|
@@ -252,30 +228,30 @@ onMount(() => { void loadPosts() })
|
|
|
252
228
|
<div class="overflow-x-auto"><table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700"><thead class="bg-gray-50 dark:bg-neutral-900/50"><tr><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Post</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Taxonomy</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Publishing</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Views</th><th class="px-4 py-3"><span class="sr-only">Actions</span></th></tr></thead><tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
|
|
253
229
|
<template :if="loading()"><tr><td colspan="5" class="px-4 py-12 text-center text-gray-500 text-sm dark:text-neutral-400"><span class="inline-block mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading posts</td></tr></template>
|
|
254
230
|
<template :else-if="visiblePosts().length === 0"><tr><td colspan="5" class="px-4 py-14 text-center"><span class="inline-block h-6 w-6 text-gray-400 i-hugeicons-quill-write-02"></span><p class="mt-2 font-medium text-gray-900 text-sm dark:text-white">No posts found</p><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the filters or create a post.</p></td></tr></template>
|
|
255
|
-
<template :else><template :for="post in visiblePosts()"><tr><td class="px-4 py-3 max-w-md"><div class="flex gap-2 items-center"><p class="font-medium text-gray-900 text-sm truncate dark:text-white">{{ post.title }}</p><span :if="post.featured" title="Featured" class="flex-shrink-0 h-4 w-4 text-amber-500 i-hugeicons-star"></span></div><p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400">{{ post.excerpt || post.content }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ authorName(post) }}</p></td><td class="px-4 py-3 max-w-xs text-gray-500 text-xs dark:text-neutral-400"><p class="truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Categories:</span> {{ optionNames(post.categoryIds, categories()) }}</p><p class="mt-1 truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Tags:</span> {{ optionNames(post.tagIds, tags()) }}</p></td><td class="px-4 py-3"><span :class="statusClass(post.status)">{{ post.status }}</span><p class="mt-2 text-gray-400 text-xs dark:text-neutral-500">{{ formatDate(post.publishedAt) }}</p></td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ formatNumber(post.views) }}</td><td class="px-4 py-3"><div class="flex justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit post ' + post.title" @click="openEdit(post)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete post ' + post.title" @click="openDelete(post)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></td></tr></template></template>
|
|
231
|
+
<template :else><template :for="post in visiblePosts()"><tr><td class="px-4 py-3 max-w-md"><div class="flex gap-2 items-center"><p class="font-medium text-gray-900 text-sm truncate dark:text-white">{{ post.title }}</p><span :if="post.featured" x-tooltip="Featured" title="Featured" class="flex-shrink-0 h-4 w-4 text-amber-500 i-hugeicons-star"></span></div><p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400">{{ post.excerpt || post.content }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ authorName(post) }}</p></td><td class="px-4 py-3 max-w-xs text-gray-500 text-xs dark:text-neutral-400"><p class="truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Categories:</span> {{ optionNames(post.categoryIds, categories()) }}</p><p class="mt-1 truncate"><span class="font-medium text-gray-700 dark:text-neutral-200">Tags:</span> {{ optionNames(post.tagIds, tags()) }}</p></td><td class="px-4 py-3"><span :class="statusClass(post.status)">{{ post.status }}</span><p class="mt-2 text-gray-400 text-xs dark:text-neutral-500">{{ formatDate(post.publishedAt) }}</p></td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ formatNumber(post.views) }}</td><td class="px-4 py-3"><div class="flex justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit post ' + post.title" @click="openEdit(post)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete post ' + post.title" @click="openDelete(post)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></td></tr></template></template>
|
|
256
232
|
</tbody></table></div>
|
|
257
233
|
</section>
|
|
258
234
|
</div>
|
|
259
235
|
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
<div class="grid gap-4 lg:grid-cols-2"><fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700"><legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Categories, {{ categoryIds().length }} selected</legend><div :if="categories().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No categories available.</div><div :else class="grid gap-2"><template :for="categoryOption in categories()"><label class="flex gap-2 items-center"><input x-model="categoryIds" type="checkbox" :value="categoryOption.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ categoryOption.name }}</span></label></template></div></fieldset><fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700"><legend class="px-1 font-semibold text-gray-700 text-sm dark:text-neutral-200">Tags, {{ tagIds().length }} selected</legend><div :if="tags().length === 0" class="text-gray-500 text-sm dark:text-neutral-400">No tags available.</div><div :else class="grid gap-2 sm:grid-cols-2"><template :for="tag in tags()"><label class="flex gap-2 items-center"><input x-model="tagIds" type="checkbox" :value="tag.id" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="text-gray-700 text-sm dark:text-neutral-200">{{ tag.name }}</span></label></template></div></fieldset></div>
|
|
272
|
-
</div></div>
|
|
273
|
-
<footer class="flex gap-2 justify-end px-6 py-4 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700"><Button :disabled="saving()" variant="secondary" @click="closeForm()">Cancel</Button><Button type="submit" :disabled="saving() || !formValid()" :loading="saving()">{{ editingPost() ? 'Save changes' : 'Create post' }}</Button></footer>
|
|
274
|
-
</form>
|
|
275
|
-
</section>
|
|
276
|
-
</div>
|
|
236
|
+
<PostEditorDialog
|
|
237
|
+
:open="formOpen()"
|
|
238
|
+
:post="editingPost()"
|
|
239
|
+
:categories="categories()"
|
|
240
|
+
:tags="tags()"
|
|
241
|
+
:authors="authors()"
|
|
242
|
+
:busy="saving()"
|
|
243
|
+
:error="saveError()"
|
|
244
|
+
@close="closeForm"
|
|
245
|
+
@submit="savePost"
|
|
246
|
+
/>
|
|
277
247
|
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
248
|
+
<ConfirmDialog
|
|
249
|
+
:isOpen="Boolean(deletingPost())"
|
|
250
|
+
title="Delete post?"
|
|
251
|
+
:description="deleteDescription()"
|
|
252
|
+
confirmLabel="Delete post"
|
|
253
|
+
:busy="deleting()"
|
|
254
|
+
:error="deleteError()"
|
|
255
|
+
@close="closeDelete"
|
|
256
|
+
@confirm="deletePost"
|
|
257
|
+
/>
|