@edgedev/create-edge-app 1.1.23 → 1.1.26
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/.env +1 -0
- package/.env.dev +1 -0
- package/README.md +55 -20
- package/{agent.md → agents.md} +2 -0
- package/bin/cli.js +6 -6
- package/edge/components/auth/login.vue +384 -0
- package/edge/components/auth/register.vue +396 -0
- package/edge/components/auth.vue +108 -0
- package/edge/components/autoFileUpload.vue +215 -0
- package/edge/components/billing.vue +8 -0
- package/edge/components/buttonDivider.vue +14 -0
- package/edge/components/chip.vue +34 -0
- package/edge/components/clipboardButton.vue +42 -0
- package/edge/components/cms/block.vue +529 -0
- package/edge/components/cms/blockApi.vue +212 -0
- package/edge/components/cms/blockEditor.vue +725 -0
- package/edge/components/cms/blockInput.vue +66 -0
- package/edge/components/cms/blockPicker.vue +486 -0
- package/edge/components/cms/blockRender.vue +78 -0
- package/edge/components/cms/blockSheetContent.vue +28 -0
- package/edge/components/cms/codeEditor.vue +466 -0
- package/edge/components/cms/fontUpload.vue +327 -0
- package/edge/components/cms/htmlContent.vue +807 -0
- package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
- package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
- package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
- package/edge/components/cms/init_blocks/carousel.html +103 -0
- package/edge/components/cms/init_blocks/contact_us.html +69 -0
- package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
- package/edge/components/cms/init_blocks/footer.html +24 -0
- package/edge/components/cms/init_blocks/header_divider.html +7 -0
- package/edge/components/cms/init_blocks/hero.html +35 -0
- package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
- package/edge/components/cms/init_blocks/newsletter.html +117 -0
- package/edge/components/cms/init_blocks/post_content.html +7 -0
- package/edge/components/cms/init_blocks/post_title_header.html +21 -0
- package/edge/components/cms/init_blocks/posts_list.html +20 -0
- package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
- package/edge/components/cms/init_blocks/property_carousel.html +59 -0
- package/edge/components/cms/init_blocks/property_detail.html +112 -0
- package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
- package/edge/components/cms/init_blocks/property_results.html +137 -0
- package/edge/components/cms/init_blocks/property_search.html +75 -0
- package/edge/components/cms/init_blocks/simple_array.html +7 -0
- package/edge/components/cms/mediaCard.vue +116 -0
- package/edge/components/cms/mediaManager.vue +386 -0
- package/edge/components/cms/menu.vue +1103 -0
- package/edge/components/cms/optionsSelect.vue +107 -0
- package/edge/components/cms/page.vue +1785 -0
- package/edge/components/cms/posts.vue +1083 -0
- package/edge/components/cms/site.vue +1298 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +426 -0
- package/edge/components/dashboard.vue +776 -0
- package/edge/components/editor.vue +671 -0
- package/edge/components/fileTree.vue +72 -0
- package/edge/components/files.vue +89 -0
- package/edge/components/formSubtypes/myOrgs.vue +214 -0
- package/edge/components/formSubtypes/users.vue +336 -0
- package/edge/components/functionChips.vue +57 -0
- package/edge/components/gError.vue +98 -0
- package/edge/components/gHelper.vue +67 -0
- package/edge/components/gInput.vue +1331 -0
- package/edge/components/loggingIn.vue +41 -0
- package/edge/components/menu.vue +137 -0
- package/edge/components/menuContent.vue +132 -0
- package/edge/components/myAccount.vue +317 -0
- package/edge/components/myOrganizations.vue +75 -0
- package/edge/components/myProfile.vue +122 -0
- package/edge/components/orgSwitcher.vue +25 -0
- package/edge/components/organizationMembers.vue +522 -0
- package/edge/components/organizationSettings.vue +271 -0
- package/edge/components/shad/breadcrumbs.vue +35 -0
- package/edge/components/shad/button.vue +43 -0
- package/edge/components/shad/checkbox.vue +73 -0
- package/edge/components/shad/combobox.vue +238 -0
- package/edge/components/shad/datepicker.vue +184 -0
- package/edge/components/shad/dialog.vue +32 -0
- package/edge/components/shad/dropdownMenu.vue +54 -0
- package/edge/components/shad/dropdownMenuItem.vue +21 -0
- package/edge/components/shad/form.vue +59 -0
- package/edge/components/shad/html.vue +877 -0
- package/edge/components/shad/input.vue +139 -0
- package/edge/components/shad/number.vue +109 -0
- package/edge/components/shad/select.vue +151 -0
- package/edge/components/shad/selectTags.vue +278 -0
- package/edge/components/shad/switch.vue +67 -0
- package/edge/components/shad/tags.vue +137 -0
- package/edge/components/shad/textarea.vue +102 -0
- package/edge/components/shad/typeMoney.vue +167 -0
- package/edge/components/sideBar.vue +288 -0
- package/edge/components/sideBarContent.vue +268 -0
- package/edge/components/sidebarProvider.vue +33 -0
- package/edge/components/tooltip.vue +16 -0
- package/edge/components/userMenu.vue +148 -0
- package/edge/components/v/alert.vue +59 -0
- package/edge/components/v/alertTitle.vue +18 -0
- package/edge/components/v/card.vue +53 -0
- package/edge/components/v/cardActions.vue +18 -0
- package/edge/components/v/cardText.vue +18 -0
- package/edge/components/v/cardTitle.vue +20 -0
- package/edge/components/v/col.vue +56 -0
- package/edge/components/v/list.vue +46 -0
- package/edge/components/v/listItem.vue +26 -0
- package/edge/components/v/listItemTitle.vue +18 -0
- package/edge/components/v/row.vue +42 -0
- package/edge/components/v/toolbar.vue +24 -0
- package/edge/composables/global.ts +519 -0
- package/edge-pull.sh +2 -0
- package/edge-push.sh +1 -0
- package/edge-status.sh +14 -0
- package/firebase.json +5 -2
- package/firebase_init.sh +21 -6
- package/package.json +1 -1
- package/plugins/firebase.client.ts +1 -0
- package/edge-components-install.sh +0 -1
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
<script setup lang="js">
|
|
2
|
+
import { computed, inject, onBeforeMount, reactive, ref, watch } from 'vue'
|
|
3
|
+
import { toTypedSchema } from '@vee-validate/zod'
|
|
4
|
+
import * as z from 'zod'
|
|
5
|
+
import { File, FileCheck, FilePen, FileWarning, Image, ImagePlus, Loader2, MoreHorizontal, Plus, Save, Trash2, X } from 'lucide-vue-next'
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
site: {
|
|
9
|
+
type: String,
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
mode: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: 'sidebar',
|
|
15
|
+
},
|
|
16
|
+
selectedPostId: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: '',
|
|
19
|
+
},
|
|
20
|
+
listVariant: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: 'sidebar',
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits(['updating', 'update:selectedPostId'])
|
|
27
|
+
|
|
28
|
+
const edgeFirebase = inject('edgeFirebase')
|
|
29
|
+
|
|
30
|
+
const collection = computed(() => `sites/${props.site}/posts`)
|
|
31
|
+
const collectionKey = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/${collection.value}`)
|
|
32
|
+
|
|
33
|
+
const publishedCollection = computed(() => `sites/${props.site}/published_posts`)
|
|
34
|
+
const publishedCollectionKey = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/${publishedCollection.value}`)
|
|
35
|
+
|
|
36
|
+
const schemas = {
|
|
37
|
+
posts: toTypedSchema(z.object({
|
|
38
|
+
name: z.string({
|
|
39
|
+
required_error: 'Name is required',
|
|
40
|
+
}).min(1, { message: 'Name is required' }),
|
|
41
|
+
title: z.string({
|
|
42
|
+
required_error: 'Title is required',
|
|
43
|
+
}).min(1, { message: 'Title is required' }),
|
|
44
|
+
tags: z.array(z.string()).optional(),
|
|
45
|
+
blurb: z.string({
|
|
46
|
+
required_error: 'Content blurb is required',
|
|
47
|
+
}).min(1, { message: 'Content blurb is required' }).max(500, { message: 'Content blurb must be at most 500 characters' }),
|
|
48
|
+
content: z.string({
|
|
49
|
+
required_error: 'Content is required',
|
|
50
|
+
}).min(1, { message: 'Content is required' }),
|
|
51
|
+
featuredImages: z.array(z.string()).optional(),
|
|
52
|
+
})),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const renameSchema = toTypedSchema(z.object({
|
|
56
|
+
name: z.string({
|
|
57
|
+
required_error: 'Name is required',
|
|
58
|
+
}).min(1, { message: 'Name is required' }),
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const isPublishedPostDiff = (postId) => {
|
|
62
|
+
const publishedPost = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published_posts`]?.[postId]
|
|
63
|
+
const draftPost = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/posts`]?.[postId]
|
|
64
|
+
if (!publishedPost && draftPost) {
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
if (publishedPost && !draftPost) {
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
if (publishedPost && draftPost) {
|
|
71
|
+
return JSON.stringify({ name: publishedPost.name, content: publishedPost.content, blurb: publishedPost.blurb, tags: publishedPost.tags, title: publishedPost.title, featuredImages: publishedPost.featuredImages }) !== JSON.stringify({ name: draftPost.name, content: draftPost.content, blurb: draftPost.blurb, tags: draftPost.tags, title: draftPost.title, featuredImages: draftPost.featuredImages })
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lastPublishedTime = (postId) => {
|
|
77
|
+
const timestamp = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[postId]?.last_updated
|
|
78
|
+
if (!timestamp)
|
|
79
|
+
return 'Never'
|
|
80
|
+
const date = new Date(timestamp)
|
|
81
|
+
return date.toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const state = reactive({
|
|
85
|
+
sheetOpen: false,
|
|
86
|
+
activePostId: '',
|
|
87
|
+
deleteDialog: false,
|
|
88
|
+
postToDelete: null,
|
|
89
|
+
editorDoc: null,
|
|
90
|
+
internalSlugUpdate: false,
|
|
91
|
+
slugManuallyEdited: false,
|
|
92
|
+
lastAutoSlug: '',
|
|
93
|
+
renameDialog: false,
|
|
94
|
+
renamePost: null,
|
|
95
|
+
renameValue: '',
|
|
96
|
+
renameSubmitting: false,
|
|
97
|
+
renameInternalUpdate: false,
|
|
98
|
+
contentImageDialog: false,
|
|
99
|
+
newDocs: {
|
|
100
|
+
posts: {
|
|
101
|
+
name: {
|
|
102
|
+
value: '',
|
|
103
|
+
cols: '12',
|
|
104
|
+
bindings: {
|
|
105
|
+
'field-type': 'text',
|
|
106
|
+
'label': 'Name',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
title: {
|
|
110
|
+
value: '',
|
|
111
|
+
cols: '12',
|
|
112
|
+
bindings: {
|
|
113
|
+
'field-type': 'text',
|
|
114
|
+
'label': 'Title',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
tags: {
|
|
118
|
+
value: [],
|
|
119
|
+
cols: '12',
|
|
120
|
+
bindings: {
|
|
121
|
+
'field-type': 'tags',
|
|
122
|
+
'value-as': 'array',
|
|
123
|
+
'label': 'Tags',
|
|
124
|
+
'placeholder': 'Add a tag',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
blurb: {
|
|
128
|
+
value: '',
|
|
129
|
+
cols: '12',
|
|
130
|
+
bindings: {
|
|
131
|
+
'field-type': 'textarea',
|
|
132
|
+
'label': 'Content Blurb / Preview',
|
|
133
|
+
'rows': '8',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
content: {
|
|
137
|
+
value: '',
|
|
138
|
+
cols: '12',
|
|
139
|
+
bindings: {
|
|
140
|
+
'field-type': 'textarea',
|
|
141
|
+
'label': 'Content',
|
|
142
|
+
'rows': '8',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
featuredImage: {
|
|
146
|
+
value: '',
|
|
147
|
+
cols: '12',
|
|
148
|
+
bindings: {
|
|
149
|
+
'field-type': 'tags',
|
|
150
|
+
'value-as': 'array',
|
|
151
|
+
'label': 'Featured Images',
|
|
152
|
+
'description': 'Enter image URLs or storage paths',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const contentEditor = ref(null)
|
|
160
|
+
|
|
161
|
+
onBeforeMount(async () => {
|
|
162
|
+
if (!edgeFirebase.data?.[collectionKey.value]) {
|
|
163
|
+
await edgeFirebase.startSnapshot(collectionKey.value)
|
|
164
|
+
}
|
|
165
|
+
if (!edgeFirebase.data?.[publishedCollectionKey.value]) {
|
|
166
|
+
await edgeFirebase.startSnapshot(publishedCollectionKey.value)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const posts = computed(() => edgeFirebase.data?.[collectionKey.value] || {})
|
|
171
|
+
const postsList = computed(() =>
|
|
172
|
+
Object.entries(posts.value)
|
|
173
|
+
.map(([id, data]) => ({ id, ...data }))
|
|
174
|
+
.sort((a, b) => (b.doc_created_at ?? 0) - (a.doc_created_at ?? 0)),
|
|
175
|
+
)
|
|
176
|
+
const hasPosts = computed(() => postsList.value.length > 0)
|
|
177
|
+
const isCreating = computed(() => state.activePostId === 'new')
|
|
178
|
+
const isFullList = computed(() => props.mode === 'list' && props.listVariant === 'full')
|
|
179
|
+
|
|
180
|
+
const getPostSlug = post => (post?.name && (typeof post.name === 'string' ? post.name.trim() : ''))
|
|
181
|
+
|
|
182
|
+
const slugify = (value) => {
|
|
183
|
+
if (!value)
|
|
184
|
+
return ''
|
|
185
|
+
return String(value)
|
|
186
|
+
.toLowerCase()
|
|
187
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
188
|
+
.replace(/(^-|-$)+/g, '')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const ensureUniqueSlug = (input, excludeId = '') => {
|
|
192
|
+
let base = slugify(input)
|
|
193
|
+
if (!base)
|
|
194
|
+
base = 'post'
|
|
195
|
+
const existing = new Set(
|
|
196
|
+
postsList.value
|
|
197
|
+
.filter(post => post.id !== excludeId)
|
|
198
|
+
.map(post => getPostSlug(post))
|
|
199
|
+
.filter(Boolean),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
let candidate = base
|
|
203
|
+
let suffix = 1
|
|
204
|
+
while (existing.has(candidate)) {
|
|
205
|
+
candidate = `${base}-${suffix}`
|
|
206
|
+
suffix += 1
|
|
207
|
+
}
|
|
208
|
+
return candidate
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const activePost = computed(() => {
|
|
212
|
+
if (!state.activePostId || state.activePostId === 'new')
|
|
213
|
+
return null
|
|
214
|
+
return posts.value?.[state.activePostId] || null
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const editorOpen = computed(() => {
|
|
218
|
+
if (props.mode === 'editor')
|
|
219
|
+
return Boolean(props.selectedPostId)
|
|
220
|
+
return state.sheetOpen
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const sheetTitle = computed(() => {
|
|
224
|
+
if (!editorOpen.value)
|
|
225
|
+
return ''
|
|
226
|
+
if (isCreating.value)
|
|
227
|
+
return 'New Post'
|
|
228
|
+
return activePost.value?.name || getPostSlug(activePost.value) || 'Edit Post'
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const currentDocId = () => (state.activePostId && (state.activePostId !== 'new' ? state.activePostId : ''))
|
|
232
|
+
|
|
233
|
+
watch(
|
|
234
|
+
() => state.editorDoc?.title,
|
|
235
|
+
(newTitle) => {
|
|
236
|
+
if (!state.editorDoc)
|
|
237
|
+
return
|
|
238
|
+
if (state.slugManuallyEdited && state.editorDoc.name)
|
|
239
|
+
return
|
|
240
|
+
if (!newTitle) {
|
|
241
|
+
state.lastAutoSlug = ''
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
const unique = ensureUniqueSlug(newTitle, currentDocId())
|
|
245
|
+
state.internalSlugUpdate = true
|
|
246
|
+
state.lastAutoSlug = unique
|
|
247
|
+
state.editorDoc.name = unique
|
|
248
|
+
state.internalSlugUpdate = false
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
watch(
|
|
253
|
+
() => state.editorDoc?.name,
|
|
254
|
+
(newName) => {
|
|
255
|
+
if (!state.editorDoc)
|
|
256
|
+
return
|
|
257
|
+
if (state.internalSlugUpdate) {
|
|
258
|
+
state.internalSlugUpdate = false
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
const sanitized = slugify(newName)
|
|
262
|
+
if (!sanitized) {
|
|
263
|
+
state.editorDoc.name = ''
|
|
264
|
+
state.slugManuallyEdited = false
|
|
265
|
+
state.lastAutoSlug = ''
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
const unique = ensureUniqueSlug(sanitized, currentDocId())
|
|
269
|
+
if (unique !== newName) {
|
|
270
|
+
state.internalSlugUpdate = true
|
|
271
|
+
state.editorDoc.name = unique
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
state.editorDoc.name = unique
|
|
275
|
+
if (unique !== state.lastAutoSlug)
|
|
276
|
+
state.slugManuallyEdited = true
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
watch(
|
|
281
|
+
() => state.renameValue,
|
|
282
|
+
(newVal) => {
|
|
283
|
+
if (!state.renameDialog)
|
|
284
|
+
return
|
|
285
|
+
if (state.renameInternalUpdate) {
|
|
286
|
+
state.renameInternalUpdate = false
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
const sanitized = slugify(newVal)
|
|
290
|
+
if (sanitized === newVal)
|
|
291
|
+
return
|
|
292
|
+
state.renameInternalUpdate = true
|
|
293
|
+
state.renameValue = sanitized
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const formatTimestamp = (input) => {
|
|
298
|
+
if (!input)
|
|
299
|
+
return 'Not yet saved'
|
|
300
|
+
try {
|
|
301
|
+
return new Date(input).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return 'Not yet saved'
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const postKey = post => post?.docId || post?.id || ''
|
|
309
|
+
const tagPreview = (tags = [], limit = 3) => {
|
|
310
|
+
const list = Array.isArray(tags) ? tags.filter(Boolean) : []
|
|
311
|
+
return {
|
|
312
|
+
visible: list.slice(0, limit),
|
|
313
|
+
remaining: Math.max(list.length - limit, 0),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const postFeaturedImage = (post) => {
|
|
317
|
+
if (post?.featuredImage)
|
|
318
|
+
return post.featuredImage
|
|
319
|
+
if (Array.isArray(post?.featuredImages) && post.featuredImages[0])
|
|
320
|
+
return post.featuredImages[0]
|
|
321
|
+
return ''
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const previewContent = (content) => {
|
|
325
|
+
if (typeof content !== 'string')
|
|
326
|
+
return ''
|
|
327
|
+
const normalized = content.trim()
|
|
328
|
+
if (!normalized)
|
|
329
|
+
return ''
|
|
330
|
+
return normalized.length > 140 ? `${normalized.slice(0, 140)}…` : normalized
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const resetEditorTracking = () => {
|
|
334
|
+
state.editorDoc = null
|
|
335
|
+
state.slugManuallyEdited = false
|
|
336
|
+
state.internalSlugUpdate = false
|
|
337
|
+
state.lastAutoSlug = ''
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const openNewPost = () => {
|
|
341
|
+
if (props.mode === 'list') {
|
|
342
|
+
emit('update:selectedPostId', 'new')
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
state.activePostId = 'new'
|
|
346
|
+
resetEditorTracking()
|
|
347
|
+
state.sheetOpen = true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const editPost = (postId) => {
|
|
351
|
+
if (props.mode === 'list') {
|
|
352
|
+
emit('update:selectedPostId', postId)
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
state.activePostId = postId
|
|
356
|
+
state.slugManuallyEdited = true
|
|
357
|
+
state.internalSlugUpdate = false
|
|
358
|
+
state.lastAutoSlug = getPostSlug(posts.value?.[postId]) || ''
|
|
359
|
+
state.sheetOpen = true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const closeSheet = () => {
|
|
363
|
+
state.sheetOpen = false
|
|
364
|
+
state.activePostId = ''
|
|
365
|
+
resetEditorTracking()
|
|
366
|
+
if (props.mode === 'editor')
|
|
367
|
+
emit('update:selectedPostId', '')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const handlePostSaved = () => {
|
|
371
|
+
console.log('Post saved')
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const openContentImageDialog = () => {
|
|
375
|
+
state.contentImageDialog = true
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const handleContentImageSelect = (url) => {
|
|
379
|
+
if (url && contentEditor.value?.insertImage) {
|
|
380
|
+
contentEditor.value.insertImage(url)
|
|
381
|
+
}
|
|
382
|
+
state.contentImageDialog = false
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const onWorkingDocUpdate = (doc) => {
|
|
386
|
+
state.editorDoc = doc
|
|
387
|
+
if (!state.slugManuallyEdited && doc?.name)
|
|
388
|
+
state.lastAutoSlug = doc.name
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
watch(
|
|
392
|
+
() => props.selectedPostId,
|
|
393
|
+
(next) => {
|
|
394
|
+
if (props.mode !== 'editor')
|
|
395
|
+
return
|
|
396
|
+
if (!next) {
|
|
397
|
+
closeSheet()
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
if (next === 'new') {
|
|
401
|
+
state.activePostId = 'new'
|
|
402
|
+
resetEditorTracking()
|
|
403
|
+
state.sheetOpen = true
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
state.activePostId = next
|
|
407
|
+
state.slugManuallyEdited = true
|
|
408
|
+
state.internalSlugUpdate = false
|
|
409
|
+
state.lastAutoSlug = getPostSlug(posts.value?.[next]) || ''
|
|
410
|
+
state.sheetOpen = true
|
|
411
|
+
},
|
|
412
|
+
{ immediate: true },
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
const openRenameDialog = (post) => {
|
|
416
|
+
const slug = getPostSlug(post)
|
|
417
|
+
const fallback = slug || ensureUniqueSlug(post?.title || post?.name || 'post', post?.id)
|
|
418
|
+
state.renamePost = {
|
|
419
|
+
id: post.id,
|
|
420
|
+
title: post.title || '',
|
|
421
|
+
currentSlug: slug,
|
|
422
|
+
}
|
|
423
|
+
state.renameSubmitting = false
|
|
424
|
+
state.renameInternalUpdate = true
|
|
425
|
+
state.renameValue = fallback
|
|
426
|
+
state.renameInternalUpdate = false
|
|
427
|
+
state.renameDialog = true
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const closeRenameDialog = () => {
|
|
431
|
+
state.renameDialog = false
|
|
432
|
+
state.renamePost = null
|
|
433
|
+
state.renameValue = ''
|
|
434
|
+
state.renameSubmitting = false
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const renamePostAction = async () => {
|
|
438
|
+
if (!state.renamePost?.id)
|
|
439
|
+
return closeRenameDialog()
|
|
440
|
+
|
|
441
|
+
state.renameSubmitting = true
|
|
442
|
+
|
|
443
|
+
let desired = slugify(state.renameValue || state.renamePost.currentSlug || state.renamePost.title || 'post')
|
|
444
|
+
if (!desired)
|
|
445
|
+
desired = 'post'
|
|
446
|
+
|
|
447
|
+
const unique = ensureUniqueSlug(desired, state.renamePost.id)
|
|
448
|
+
|
|
449
|
+
if (unique === state.renamePost.currentSlug) {
|
|
450
|
+
state.renameSubmitting = false
|
|
451
|
+
closeRenameDialog()
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
await edgeFirebase.changeDoc(collectionKey.value, state.renamePost.id, { name: unique })
|
|
457
|
+
state.renameValue = unique
|
|
458
|
+
closeRenameDialog()
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.error('Failed to rename post:', error)
|
|
462
|
+
state.renameSubmitting = false
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const showDeleteDialog = (post) => {
|
|
467
|
+
state.postToDelete = {
|
|
468
|
+
id: post.id,
|
|
469
|
+
name: post.title || getPostSlug(post) || 'Untitled Post',
|
|
470
|
+
}
|
|
471
|
+
state.deleteDialog = true
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const deletePost = async () => {
|
|
475
|
+
const target = state.postToDelete
|
|
476
|
+
if (!target?.id) {
|
|
477
|
+
state.deleteDialog = false
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const postId = target.id
|
|
482
|
+
try {
|
|
483
|
+
await edgeFirebase.removeDoc(collectionKey.value, postId)
|
|
484
|
+
await edgeFirebase.removeDoc(publishedCollectionKey.value, postId)
|
|
485
|
+
if (state.activePostId === postId)
|
|
486
|
+
closeSheet()
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error('Failed to delete post:', error)
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
state.deleteDialog = false
|
|
493
|
+
state.postToDelete = null
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const addTag = async (tag) => {
|
|
498
|
+
console.log('Tag to add:', tag)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const getTagsFromPosts = computed(() => {
|
|
502
|
+
const tagMap = new Map()
|
|
503
|
+
postsList.value.forEach((post) => {
|
|
504
|
+
if (Array.isArray(post.tags)) {
|
|
505
|
+
post.tags.forEach((tag) => {
|
|
506
|
+
if (tag && typeof tag === 'string' && !tagMap.has(tag)) {
|
|
507
|
+
tagMap.set(tag, { name: tag, title: tag })
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
return Array.from(tagMap.values()).sort((a, b) => a.title.localeCompare(b.title))
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
const publishPost = async (postId) => {
|
|
516
|
+
emit('updating', true)
|
|
517
|
+
if (!postId)
|
|
518
|
+
return
|
|
519
|
+
const post = posts.value?.[postId]
|
|
520
|
+
if (!post)
|
|
521
|
+
return
|
|
522
|
+
try {
|
|
523
|
+
await edgeFirebase.storeDoc(publishedCollectionKey.value, post)
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
console.error('Failed to publish post:', error)
|
|
527
|
+
}
|
|
528
|
+
emit('updating', false)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const unPublishPost = async (postId) => {
|
|
532
|
+
if (!postId)
|
|
533
|
+
return
|
|
534
|
+
try {
|
|
535
|
+
await edgeFirebase.removeDoc(publishedCollectionKey.value, postId)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
catch (error) {
|
|
539
|
+
console.error('Failed to unpublish post:', error)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
</script>
|
|
543
|
+
|
|
544
|
+
<template>
|
|
545
|
+
<div v-if="props.mode !== 'editor'" class="space-y-4">
|
|
546
|
+
<edge-shad-button
|
|
547
|
+
variant="outline"
|
|
548
|
+
:class="isFullList ? 'h-8 px-3' : 'w-full mt-2 py-0 h-[28px]'"
|
|
549
|
+
@click="openNewPost"
|
|
550
|
+
>
|
|
551
|
+
<Plus class="h-4 w-4" />
|
|
552
|
+
New Post
|
|
553
|
+
</edge-shad-button>
|
|
554
|
+
|
|
555
|
+
<div v-if="isFullList" class="rounded-lg border bg-card overflow-hidden">
|
|
556
|
+
<div class="flex items-center justify-between px-4 py-3 border-b bg-muted/40">
|
|
557
|
+
<div class="text-sm font-semibold">
|
|
558
|
+
Posts
|
|
559
|
+
</div>
|
|
560
|
+
<div class="text-xs text-muted-foreground">
|
|
561
|
+
{{ postsList.length }} total
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
<div v-if="hasPosts" class="divide-y">
|
|
565
|
+
<div
|
|
566
|
+
v-for="post in postsList"
|
|
567
|
+
:key="post.id"
|
|
568
|
+
class="px-4 py-3 hover:bg-muted/40 cursor-pointer"
|
|
569
|
+
@click="editPost(post.id)"
|
|
570
|
+
>
|
|
571
|
+
<div class="flex items-start gap-4">
|
|
572
|
+
<div class="h-16 w-20 rounded-md border bg-muted/40 overflow-hidden flex items-center justify-center shrink-0">
|
|
573
|
+
<img
|
|
574
|
+
v-if="postFeaturedImage(post)"
|
|
575
|
+
:src="postFeaturedImage(post)"
|
|
576
|
+
alt=""
|
|
577
|
+
class="h-full w-full object-cover"
|
|
578
|
+
>
|
|
579
|
+
<Image v-else class="h-6 w-6 text-muted-foreground/60" />
|
|
580
|
+
</div>
|
|
581
|
+
<div class="flex-1 min-w-0">
|
|
582
|
+
<div class="flex items-start justify-between gap-3">
|
|
583
|
+
<div class="min-w-0 space-y-1">
|
|
584
|
+
<div class="text-sm font-medium text-foreground truncate">
|
|
585
|
+
{{ post.title || post.name || 'Untitled Post' }}
|
|
586
|
+
</div>
|
|
587
|
+
<div class="text-xs text-muted-foreground line-clamp-2">
|
|
588
|
+
{{ previewContent(post.blurb || post.content) || 'No content yet.' }}
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
<DropdownMenu>
|
|
592
|
+
<DropdownMenuTrigger as-child>
|
|
593
|
+
<edge-shad-button variant="ghost" size="icon" class="h-8 w-8" @click.stop>
|
|
594
|
+
<MoreHorizontal class="h-4 w-4" />
|
|
595
|
+
</edge-shad-button>
|
|
596
|
+
</DropdownMenuTrigger>
|
|
597
|
+
<DropdownMenuContent side="right" align="start">
|
|
598
|
+
<DropdownMenuItem @click="openRenameDialog(post)">
|
|
599
|
+
<FilePen class="h-4 w-4" />
|
|
600
|
+
Rename
|
|
601
|
+
</DropdownMenuItem>
|
|
602
|
+
<DropdownMenuItem v-if="isPublishedPostDiff(postKey(post))" @click="publishPost(postKey(post))">
|
|
603
|
+
<FileCheck class="h-4 w-4" />
|
|
604
|
+
Publish
|
|
605
|
+
</DropdownMenuItem>
|
|
606
|
+
<DropdownMenuItem v-else @click="unPublishPost(postKey(post))">
|
|
607
|
+
<FileWarning class="h-4 w-4" />
|
|
608
|
+
Unpublish
|
|
609
|
+
</DropdownMenuItem>
|
|
610
|
+
<DropdownMenuItem class="text-destructive" @click="showDeleteDialog(post)">
|
|
611
|
+
<Trash2 class="h-4 w-4" />
|
|
612
|
+
Delete
|
|
613
|
+
</DropdownMenuItem>
|
|
614
|
+
</DropdownMenuContent>
|
|
615
|
+
</DropdownMenu>
|
|
616
|
+
</div>
|
|
617
|
+
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
618
|
+
<div class="flex items-center gap-1">
|
|
619
|
+
<FileWarning v-if="isPublishedPostDiff(postKey(post))" class="h-3.5 w-3.5 text-yellow-600" />
|
|
620
|
+
<FileCheck v-else class="h-3.5 w-3.5 text-green-700" />
|
|
621
|
+
<span>{{ isPublishedPostDiff(postKey(post)) ? 'Draft' : 'Published' }}</span>
|
|
622
|
+
</div>
|
|
623
|
+
<span>{{ formatTimestamp(post.last_updated || post.doc_created_at) }}</span>
|
|
624
|
+
<div v-if="Array.isArray(post.tags) && post.tags.length" class="flex flex-wrap items-center gap-1">
|
|
625
|
+
<span
|
|
626
|
+
v-for="tag in tagPreview(post.tags).visible"
|
|
627
|
+
:key="tag"
|
|
628
|
+
class="rounded-full bg-secondary px-2 py-0.5 text-[10px] text-secondary-foreground"
|
|
629
|
+
>
|
|
630
|
+
{{ tag }}
|
|
631
|
+
</span>
|
|
632
|
+
<span
|
|
633
|
+
v-if="tagPreview(post.tags).remaining"
|
|
634
|
+
class="rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground"
|
|
635
|
+
>
|
|
636
|
+
+{{ tagPreview(post.tags).remaining }}
|
|
637
|
+
</span>
|
|
638
|
+
</div>
|
|
639
|
+
<span v-if="Array.isArray(post.featuredImages) && post.featuredImages.length">
|
|
640
|
+
{{ post.featuredImages.length }} featured image{{ post.featuredImages.length > 1 ? 's' : '' }}
|
|
641
|
+
</span>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
<div
|
|
648
|
+
v-else
|
|
649
|
+
class="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center"
|
|
650
|
+
>
|
|
651
|
+
<File class="h-8 w-8 text-muted-foreground/60" />
|
|
652
|
+
<div class="space-y-1">
|
|
653
|
+
<h3 class="text-base font-medium">
|
|
654
|
+
No posts yet
|
|
655
|
+
</h3>
|
|
656
|
+
<p class="text-sm text-muted-foreground">
|
|
657
|
+
Create your first post to start publishing content.
|
|
658
|
+
</p>
|
|
659
|
+
</div>
|
|
660
|
+
<edge-shad-button variant="outline" class="gap-2" @click="openNewPost">
|
|
661
|
+
<Plus class="h-4 w-4" />
|
|
662
|
+
New Post
|
|
663
|
+
</edge-shad-button>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<div v-else>
|
|
668
|
+
<div v-if="hasPosts" class="space-y-2 hidden">
|
|
669
|
+
<SidebarMenuItem v-for="post in postsList" :key="post.id">
|
|
670
|
+
<SidebarMenuButton class="!px-0 hover:!bg-transparent" @click="editPost(post.id)">
|
|
671
|
+
<div class="h-8 w-8 rounded-md border bg-muted/40 overflow-hidden flex items-center justify-center shrink-0">
|
|
672
|
+
<img
|
|
673
|
+
v-if="postFeaturedImage(post)"
|
|
674
|
+
:src="postFeaturedImage(post)"
|
|
675
|
+
alt=""
|
|
676
|
+
class="h-full w-full object-cover"
|
|
677
|
+
>
|
|
678
|
+
<Image v-else class="h-4 w-4 text-muted-foreground/60" />
|
|
679
|
+
</div>
|
|
680
|
+
<FileWarning v-if="isPublishedPostDiff(postKey(post))" class="!text-yellow-600 ml-2" />
|
|
681
|
+
<FileCheck v-else class="text-xs !text-green-700 font-normal ml-2" />
|
|
682
|
+
<div class="ml-2 flex flex-col text-left">
|
|
683
|
+
<span class="text-sm font-medium">{{ post.name || 'Untitled Post' }}</span>
|
|
684
|
+
</div>
|
|
685
|
+
</SidebarMenuButton>
|
|
686
|
+
<SidebarGroupAction class="absolute right-2 top-0 hover:!bg-transparent">
|
|
687
|
+
<DropdownMenu>
|
|
688
|
+
<DropdownMenuTrigger as-child>
|
|
689
|
+
<SidebarMenuAction>
|
|
690
|
+
<MoreHorizontal />
|
|
691
|
+
</SidebarMenuAction>
|
|
692
|
+
</DropdownMenuTrigger>
|
|
693
|
+
<DropdownMenuContent side="right" align="start">
|
|
694
|
+
<DropdownMenuItem @click="openRenameDialog(post)">
|
|
695
|
+
<FilePen class="h-4 w-4" />
|
|
696
|
+
Rename
|
|
697
|
+
</DropdownMenuItem>
|
|
698
|
+
<DropdownMenuItem v-if="isPublishedPostDiff(postKey(post))" @click="publishPost(postKey(post))">
|
|
699
|
+
<FileCheck class="h-4 w-4" />
|
|
700
|
+
Publish
|
|
701
|
+
</DropdownMenuItem>
|
|
702
|
+
<DropdownMenuItem v-else @click="unPublishPost(postKey(post))">
|
|
703
|
+
<FileWarning class="h-4 w-4" />
|
|
704
|
+
Unpublish
|
|
705
|
+
</DropdownMenuItem>
|
|
706
|
+
|
|
707
|
+
<DropdownMenuItem class="text-destructive" @click="showDeleteDialog(post)">
|
|
708
|
+
<Trash2 class="h-4 w-4" />
|
|
709
|
+
Delete
|
|
710
|
+
</DropdownMenuItem>
|
|
711
|
+
</DropdownMenuContent>
|
|
712
|
+
</DropdownMenu>
|
|
713
|
+
</SidebarGroupAction>
|
|
714
|
+
<div class="w-full pl-7 pb-2 text-xs text-muted-foreground cursor-pointer" @click="editPost(post.id)">
|
|
715
|
+
<div>{{ formatTimestamp(post.last_updated || post.doc_created_at) }}</div>
|
|
716
|
+
<div v-if="Array.isArray(post.tags) && post.tags.length" class="mt-1 flex flex-wrap gap-1">
|
|
717
|
+
<span
|
|
718
|
+
v-for="tag in tagPreview(post.tags).visible"
|
|
719
|
+
:key="tag"
|
|
720
|
+
class="rounded-full bg-secondary px-2 py-0.5 text-[10px] text-secondary-foreground"
|
|
721
|
+
>
|
|
722
|
+
{{ tag }}
|
|
723
|
+
</span>
|
|
724
|
+
<span
|
|
725
|
+
v-if="tagPreview(post.tags).remaining"
|
|
726
|
+
class="rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground"
|
|
727
|
+
>
|
|
728
|
+
+{{ tagPreview(post.tags).remaining }}
|
|
729
|
+
</span>
|
|
730
|
+
</div>
|
|
731
|
+
<div v-if="Array.isArray(post.featuredImages) && post.featuredImages.length" class="mt-1 text-[11px]">
|
|
732
|
+
{{ post.featuredImages.length }} featured image{{ post.featuredImages.length > 1 ? 's' : '' }}
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
<Separator class="my-2" />
|
|
736
|
+
</SidebarMenuItem>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<div
|
|
740
|
+
v-else
|
|
741
|
+
class="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-muted-foreground/40 px-6 py-10 text-center"
|
|
742
|
+
>
|
|
743
|
+
<File class="h-8 w-8 text-muted-foreground/60" />
|
|
744
|
+
<div class="space-y-1">
|
|
745
|
+
<h3 class="text-base font-medium">
|
|
746
|
+
No posts yet
|
|
747
|
+
</h3>
|
|
748
|
+
<p class="text-sm text-muted-foreground">
|
|
749
|
+
Create your first post to start publishing content.
|
|
750
|
+
</p>
|
|
751
|
+
</div>
|
|
752
|
+
<edge-shad-button variant="outline" class="gap-2" @click="openNewPost">
|
|
753
|
+
<Plus class="h-4 w-4" />
|
|
754
|
+
New Post
|
|
755
|
+
</edge-shad-button>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
|
|
760
|
+
<edge-shad-dialog v-model="state.deleteDialog">
|
|
761
|
+
<DialogContent class="pt-10">
|
|
762
|
+
<DialogHeader>
|
|
763
|
+
<DialogTitle class="text-left">
|
|
764
|
+
Delete "{{ state.postToDelete?.name || 'this post' }}"?
|
|
765
|
+
</DialogTitle>
|
|
766
|
+
<DialogDescription>
|
|
767
|
+
This action cannot be undone.
|
|
768
|
+
</DialogDescription>
|
|
769
|
+
</DialogHeader>
|
|
770
|
+
<DialogFooter class="flex justify-between pt-2">
|
|
771
|
+
<edge-shad-button variant="outline" @click="state.deleteDialog = false">
|
|
772
|
+
Cancel
|
|
773
|
+
</edge-shad-button>
|
|
774
|
+
<edge-shad-button variant="destructive" class="w-full" @click="deletePost">
|
|
775
|
+
Delete
|
|
776
|
+
</edge-shad-button>
|
|
777
|
+
</DialogFooter>
|
|
778
|
+
</DialogContent>
|
|
779
|
+
</edge-shad-dialog>
|
|
780
|
+
|
|
781
|
+
<edge-shad-dialog v-model="state.renameDialog">
|
|
782
|
+
<DialogContent class="pt-10">
|
|
783
|
+
<edge-shad-form :schema="renameSchema" @submit="renamePostAction">
|
|
784
|
+
<DialogHeader>
|
|
785
|
+
<DialogTitle class="text-left">
|
|
786
|
+
Rename "{{ state.renamePost?.title || state.renamePost?.currentSlug || 'Post' }}"
|
|
787
|
+
</DialogTitle>
|
|
788
|
+
<DialogDescription>
|
|
789
|
+
Update the slug used in URLs. Existing links will change after renaming.
|
|
790
|
+
</DialogDescription>
|
|
791
|
+
</DialogHeader>
|
|
792
|
+
<edge-shad-input v-model="state.renameValue" name="name" label="Name" />
|
|
793
|
+
<DialogFooter class="flex justify-between pt-2">
|
|
794
|
+
<edge-shad-button variant="outline" @click="closeRenameDialog">
|
|
795
|
+
Cancel
|
|
796
|
+
</edge-shad-button>
|
|
797
|
+
<edge-shad-button
|
|
798
|
+
type="submit"
|
|
799
|
+
class="w-full bg-slate-800 text-white hover:bg-slate-400"
|
|
800
|
+
:disabled="state.renameSubmitting"
|
|
801
|
+
>
|
|
802
|
+
<Loader2 v-if="state.renameSubmitting" class="h-4 w-4 animate-spin" />
|
|
803
|
+
<span v-else>Rename</span>
|
|
804
|
+
</edge-shad-button>
|
|
805
|
+
</DialogFooter>
|
|
806
|
+
</edge-shad-form>
|
|
807
|
+
</DialogContent>
|
|
808
|
+
</edge-shad-dialog>
|
|
809
|
+
|
|
810
|
+
<template v-if="props.mode === 'editor'">
|
|
811
|
+
<div v-if="editorOpen" class="h-full flex flex-col bg-background px-0">
|
|
812
|
+
<edge-editor
|
|
813
|
+
v-if="editorOpen"
|
|
814
|
+
:collection="collection"
|
|
815
|
+
:doc-id="state.activePostId"
|
|
816
|
+
:schema="schemas.posts"
|
|
817
|
+
:new-doc-schema="state.newDocs.posts"
|
|
818
|
+
class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
|
|
819
|
+
card-content-class="px-0"
|
|
820
|
+
:show-header="true"
|
|
821
|
+
:no-close-after-save="true"
|
|
822
|
+
:save-function-override="handlePostSaved"
|
|
823
|
+
@working-doc="onWorkingDocUpdate"
|
|
824
|
+
>
|
|
825
|
+
<template #header="slotProps">
|
|
826
|
+
<div class="relative flex items-center bg-secondary p-2 justify-between sticky top-0 z-50 bg-primary rounded h-[50px]">
|
|
827
|
+
<span class="text-lg font-semibold whitespace-nowrap pr-1">{{ sheetTitle }}</span>
|
|
828
|
+
<div class="flex w-full items-center">
|
|
829
|
+
<div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
|
|
830
|
+
<div class="flex items-center gap-1 pr-3">
|
|
831
|
+
<edge-shad-button
|
|
832
|
+
v-if="!slotProps.unsavedChanges"
|
|
833
|
+
variant="text"
|
|
834
|
+
class="hover:text-red-700/50 text-xs h-[26px] text-red-700"
|
|
835
|
+
@click="closeSheet"
|
|
836
|
+
>
|
|
837
|
+
<X class="w-4 h-4" />
|
|
838
|
+
Close
|
|
839
|
+
</edge-shad-button>
|
|
840
|
+
<edge-shad-button
|
|
841
|
+
v-else
|
|
842
|
+
variant="text"
|
|
843
|
+
class="hover:text-red-700/50 text-xs h-[26px] text-red-700"
|
|
844
|
+
@click="closeSheet"
|
|
845
|
+
>
|
|
846
|
+
<X class="w-4 h-4" />
|
|
847
|
+
Cancel
|
|
848
|
+
</edge-shad-button>
|
|
849
|
+
<edge-shad-button
|
|
850
|
+
v-if="isCreating || slotProps.unsavedChanges"
|
|
851
|
+
variant="text"
|
|
852
|
+
type="submit"
|
|
853
|
+
class="bg-secondary hover:text-primary/50 text-xs h-[26px] text-primary"
|
|
854
|
+
:disabled="slotProps.submitting"
|
|
855
|
+
>
|
|
856
|
+
<Loader2 v-if="slotProps.submitting" class="w-4 h-4 animate-spin" />
|
|
857
|
+
<Save v-else class="w-4 h-4" />
|
|
858
|
+
<span>Save</span>
|
|
859
|
+
</edge-shad-button>
|
|
860
|
+
</div>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
</template>
|
|
864
|
+
<template #main="slotProps">
|
|
865
|
+
<div class="p-6 h-[calc(100vh-122px)] overflow-y-auto">
|
|
866
|
+
<div class="grid gap-6 lg:grid-cols-[320px_minmax(0,1fr)]">
|
|
867
|
+
<div class="space-y-6">
|
|
868
|
+
<div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
|
|
869
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
870
|
+
Post Details
|
|
871
|
+
</div>
|
|
872
|
+
<edge-shad-input
|
|
873
|
+
v-model="slotProps.workingDoc.name"
|
|
874
|
+
name="name"
|
|
875
|
+
label="Name (slug used in URL)"
|
|
876
|
+
/>
|
|
877
|
+
<edge-shad-input
|
|
878
|
+
v-model="slotProps.workingDoc.title"
|
|
879
|
+
name="title"
|
|
880
|
+
label="Title"
|
|
881
|
+
:disabled="slotProps.submitting"
|
|
882
|
+
/>
|
|
883
|
+
<edge-shad-select-tags
|
|
884
|
+
v-model="slotProps.workingDoc.tags"
|
|
885
|
+
name="tags"
|
|
886
|
+
label="Tags"
|
|
887
|
+
placeholder="Add a tag"
|
|
888
|
+
:disabled="slotProps.submitting"
|
|
889
|
+
:items="getTagsFromPosts"
|
|
890
|
+
:allow-additions="true"
|
|
891
|
+
@add="addTag"
|
|
892
|
+
/>
|
|
893
|
+
</div>
|
|
894
|
+
<div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
|
|
895
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
896
|
+
Featured Image
|
|
897
|
+
</div>
|
|
898
|
+
<div class="relative bg-muted/50 py-2 h-48 rounded-lg flex items-center justify-center hover:opacity-80 transition-opacity cursor-pointer">
|
|
899
|
+
<div class="bg-black/80 absolute left-0 top-0 w-full h-full opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center z-10 cursor-pointer rounded-lg">
|
|
900
|
+
<Dialog v-model:open="state.imageOpen">
|
|
901
|
+
<DialogTrigger as-child>
|
|
902
|
+
<edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
|
|
903
|
+
<ImagePlus class="h-5 w-5" />
|
|
904
|
+
Select Image
|
|
905
|
+
</edge-shad-button>
|
|
906
|
+
</DialogTrigger>
|
|
907
|
+
<DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
|
|
908
|
+
<DialogHeader>
|
|
909
|
+
<DialogTitle>Select Image</DialogTitle>
|
|
910
|
+
<DialogDescription />
|
|
911
|
+
</DialogHeader>
|
|
912
|
+
<edge-cms-media-manager
|
|
913
|
+
:site="props.site"
|
|
914
|
+
:select-mode="true"
|
|
915
|
+
@select="(url) => { slotProps.workingDoc.featuredImage = url; state.imageOpen = false; }"
|
|
916
|
+
/>
|
|
917
|
+
</DialogContent>
|
|
918
|
+
</Dialog>
|
|
919
|
+
</div>
|
|
920
|
+
<img v-if="slotProps.workingDoc.featuredImage" :src="slotProps.workingDoc.featuredImage" class="mb-2 max-h-40 mx-auto object-contain">
|
|
921
|
+
<span v-else class="text-sm text-muted-foreground italic">No featured image selected</span>
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
<div class="space-y-6">
|
|
926
|
+
<div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
|
|
927
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
928
|
+
Content
|
|
929
|
+
</div>
|
|
930
|
+
<edge-shad-textarea
|
|
931
|
+
v-model="slotProps.workingDoc.blurb"
|
|
932
|
+
name="blurb"
|
|
933
|
+
label="Content Blurb / Preview"
|
|
934
|
+
:disabled="slotProps.submitting"
|
|
935
|
+
rows="6"
|
|
936
|
+
/>
|
|
937
|
+
<edge-shad-html
|
|
938
|
+
ref="contentEditor"
|
|
939
|
+
v-model="slotProps.workingDoc.content"
|
|
940
|
+
height-class="h-[calc(100vh-490px)]"
|
|
941
|
+
:enabled-toggles="['bold', 'italic', 'strike', 'bulletlist', 'orderedlist', 'underline', 'image']"
|
|
942
|
+
name="content"
|
|
943
|
+
label="Content"
|
|
944
|
+
:disabled="slotProps.submitting"
|
|
945
|
+
@request-image="openContentImageDialog"
|
|
946
|
+
/>
|
|
947
|
+
<Dialog v-model:open="state.contentImageDialog">
|
|
948
|
+
<DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
|
|
949
|
+
<DialogHeader>
|
|
950
|
+
<DialogTitle>Select Image</DialogTitle>
|
|
951
|
+
<DialogDescription />
|
|
952
|
+
</DialogHeader>
|
|
953
|
+
<edge-cms-media-manager
|
|
954
|
+
:site="props.site"
|
|
955
|
+
:select-mode="true"
|
|
956
|
+
@select="handleContentImageSelect"
|
|
957
|
+
/>
|
|
958
|
+
</DialogContent>
|
|
959
|
+
</Dialog>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
</template>
|
|
965
|
+
<template #footer>
|
|
966
|
+
<div />
|
|
967
|
+
</template>
|
|
968
|
+
</edge-editor>
|
|
969
|
+
</div>
|
|
970
|
+
</template>
|
|
971
|
+
<Sheet v-else v-model:open="state.sheetOpen">
|
|
972
|
+
<SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
|
|
973
|
+
<SheetHeader>
|
|
974
|
+
<SheetTitle>{{ sheetTitle }}</SheetTitle>
|
|
975
|
+
</SheetHeader>
|
|
976
|
+
<edge-editor
|
|
977
|
+
v-if="editorOpen"
|
|
978
|
+
:collection="collection"
|
|
979
|
+
:doc-id="state.activePostId"
|
|
980
|
+
:schema="schemas.posts"
|
|
981
|
+
:new-doc-schema="state.newDocs.posts"
|
|
982
|
+
class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0"
|
|
983
|
+
card-content-class="px-0"
|
|
984
|
+
:show-header="false"
|
|
985
|
+
:no-close-after-save="true"
|
|
986
|
+
:save-function-override="handlePostSaved"
|
|
987
|
+
@working-doc="onWorkingDocUpdate"
|
|
988
|
+
>
|
|
989
|
+
<template #main="slotProps">
|
|
990
|
+
<div class="p-6 space-y-4 h-[calc(100vh-122px)] overflow-y-auto">
|
|
991
|
+
<edge-shad-input
|
|
992
|
+
v-model="slotProps.workingDoc.name"
|
|
993
|
+
name="name"
|
|
994
|
+
label="Name"
|
|
995
|
+
/>
|
|
996
|
+
<edge-shad-input
|
|
997
|
+
v-model="slotProps.workingDoc.title"
|
|
998
|
+
name="title"
|
|
999
|
+
label="Title"
|
|
1000
|
+
:disabled="slotProps.submitting"
|
|
1001
|
+
/>
|
|
1002
|
+
<div class="relative bg-muted py-2 h-48 rounded-md flex items-center justify-center hover:opacity-80 transition-opacity cursor-pointer">
|
|
1003
|
+
<div class="bg-black/80 absolute left-0 top-0 w-full h-full opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center z-10 cursor-pointer">
|
|
1004
|
+
<Dialog v-model:open="state.imageOpen">
|
|
1005
|
+
<DialogTrigger as-child>
|
|
1006
|
+
<edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
|
|
1007
|
+
<ImagePlus class="h-5 w-5" />
|
|
1008
|
+
Select Image
|
|
1009
|
+
</edge-shad-button>
|
|
1010
|
+
</DialogTrigger>
|
|
1011
|
+
<DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
|
|
1012
|
+
<DialogHeader>
|
|
1013
|
+
<DialogTitle>Select Image</DialogTitle>
|
|
1014
|
+
<DialogDescription />
|
|
1015
|
+
</DialogHeader>
|
|
1016
|
+
<edge-cms-media-manager
|
|
1017
|
+
:site="props.site"
|
|
1018
|
+
:select-mode="true"
|
|
1019
|
+
@select="(url) => { slotProps.workingDoc.featuredImage = url; state.imageOpen = false; }"
|
|
1020
|
+
/>
|
|
1021
|
+
</DialogContent>
|
|
1022
|
+
</Dialog>
|
|
1023
|
+
</div>
|
|
1024
|
+
<img v-if="slotProps.workingDoc.featuredImage" :src="slotProps.workingDoc.featuredImage" class="mb-2 max-h-40 mx-auto object-contain">
|
|
1025
|
+
<span v-else class="text-sm text-muted-foreground italic">No featured image selected, click to select</span>
|
|
1026
|
+
</div>
|
|
1027
|
+
<edge-shad-select-tags
|
|
1028
|
+
v-model="slotProps.workingDoc.tags"
|
|
1029
|
+
name="tags"
|
|
1030
|
+
label="Tags"
|
|
1031
|
+
placeholder="Add a tag"
|
|
1032
|
+
:disabled="slotProps.submitting"
|
|
1033
|
+
:items="getTagsFromPosts"
|
|
1034
|
+
:allow-additions="true"
|
|
1035
|
+
@add="addTag"
|
|
1036
|
+
/>
|
|
1037
|
+
<edge-shad-textarea
|
|
1038
|
+
v-model="slotProps.workingDoc.blurb"
|
|
1039
|
+
name="blurb"
|
|
1040
|
+
label="Content Blurb / Preview"
|
|
1041
|
+
:disabled="slotProps.submitting"
|
|
1042
|
+
rows="8"
|
|
1043
|
+
/>
|
|
1044
|
+
<edge-shad-html
|
|
1045
|
+
ref="contentEditor"
|
|
1046
|
+
v-model="slotProps.workingDoc.content"
|
|
1047
|
+
:enabled-toggles="['bold', 'italic', 'strike', 'bulletlist', 'orderedlist', 'underline', 'image']"
|
|
1048
|
+
name="content"
|
|
1049
|
+
label="Content"
|
|
1050
|
+
:disabled="slotProps.submitting"
|
|
1051
|
+
@request-image="openContentImageDialog"
|
|
1052
|
+
/>
|
|
1053
|
+
<Dialog v-model:open="state.contentImageDialog">
|
|
1054
|
+
<DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
|
|
1055
|
+
<DialogHeader>
|
|
1056
|
+
<DialogTitle>Select Image</DialogTitle>
|
|
1057
|
+
<DialogDescription />
|
|
1058
|
+
</DialogHeader>
|
|
1059
|
+
<edge-cms-media-manager
|
|
1060
|
+
:site="props.site"
|
|
1061
|
+
:select-mode="true"
|
|
1062
|
+
@select="handleContentImageSelect"
|
|
1063
|
+
/>
|
|
1064
|
+
</DialogContent>
|
|
1065
|
+
</Dialog>
|
|
1066
|
+
</div>
|
|
1067
|
+
<SheetFooter class="pt-2 flex justify-between">
|
|
1068
|
+
<edge-shad-button variant="destructive" class="text-white" @click="state.sheetOpen = false">
|
|
1069
|
+
Cancel
|
|
1070
|
+
</edge-shad-button>
|
|
1071
|
+
<edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1072
|
+
<Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
|
|
1073
|
+
Save
|
|
1074
|
+
</edge-shad-button>
|
|
1075
|
+
</SheetFooter>
|
|
1076
|
+
</template>
|
|
1077
|
+
<template #footer>
|
|
1078
|
+
<div />
|
|
1079
|
+
</template>
|
|
1080
|
+
</edge-editor>
|
|
1081
|
+
</SheetContent>
|
|
1082
|
+
</Sheet>
|
|
1083
|
+
</template>
|