@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.
Files changed (116) hide show
  1. package/.env +1 -0
  2. package/.env.dev +1 -0
  3. package/README.md +55 -20
  4. package/{agent.md → agents.md} +2 -0
  5. package/bin/cli.js +6 -6
  6. package/edge/components/auth/login.vue +384 -0
  7. package/edge/components/auth/register.vue +396 -0
  8. package/edge/components/auth.vue +108 -0
  9. package/edge/components/autoFileUpload.vue +215 -0
  10. package/edge/components/billing.vue +8 -0
  11. package/edge/components/buttonDivider.vue +14 -0
  12. package/edge/components/chip.vue +34 -0
  13. package/edge/components/clipboardButton.vue +42 -0
  14. package/edge/components/cms/block.vue +529 -0
  15. package/edge/components/cms/blockApi.vue +212 -0
  16. package/edge/components/cms/blockEditor.vue +725 -0
  17. package/edge/components/cms/blockInput.vue +66 -0
  18. package/edge/components/cms/blockPicker.vue +486 -0
  19. package/edge/components/cms/blockRender.vue +78 -0
  20. package/edge/components/cms/blockSheetContent.vue +28 -0
  21. package/edge/components/cms/codeEditor.vue +466 -0
  22. package/edge/components/cms/fontUpload.vue +327 -0
  23. package/edge/components/cms/htmlContent.vue +807 -0
  24. package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
  25. package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
  26. package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
  27. package/edge/components/cms/init_blocks/carousel.html +103 -0
  28. package/edge/components/cms/init_blocks/contact_us.html +69 -0
  29. package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
  30. package/edge/components/cms/init_blocks/footer.html +24 -0
  31. package/edge/components/cms/init_blocks/header_divider.html +7 -0
  32. package/edge/components/cms/init_blocks/hero.html +35 -0
  33. package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
  34. package/edge/components/cms/init_blocks/newsletter.html +117 -0
  35. package/edge/components/cms/init_blocks/post_content.html +7 -0
  36. package/edge/components/cms/init_blocks/post_title_header.html +21 -0
  37. package/edge/components/cms/init_blocks/posts_list.html +20 -0
  38. package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
  39. package/edge/components/cms/init_blocks/property_carousel.html +59 -0
  40. package/edge/components/cms/init_blocks/property_detail.html +112 -0
  41. package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
  42. package/edge/components/cms/init_blocks/property_results.html +137 -0
  43. package/edge/components/cms/init_blocks/property_search.html +75 -0
  44. package/edge/components/cms/init_blocks/simple_array.html +7 -0
  45. package/edge/components/cms/mediaCard.vue +116 -0
  46. package/edge/components/cms/mediaManager.vue +386 -0
  47. package/edge/components/cms/menu.vue +1103 -0
  48. package/edge/components/cms/optionsSelect.vue +107 -0
  49. package/edge/components/cms/page.vue +1785 -0
  50. package/edge/components/cms/posts.vue +1083 -0
  51. package/edge/components/cms/site.vue +1298 -0
  52. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  53. package/edge/components/cms/themeEditor.vue +426 -0
  54. package/edge/components/dashboard.vue +776 -0
  55. package/edge/components/editor.vue +671 -0
  56. package/edge/components/fileTree.vue +72 -0
  57. package/edge/components/files.vue +89 -0
  58. package/edge/components/formSubtypes/myOrgs.vue +214 -0
  59. package/edge/components/formSubtypes/users.vue +336 -0
  60. package/edge/components/functionChips.vue +57 -0
  61. package/edge/components/gError.vue +98 -0
  62. package/edge/components/gHelper.vue +67 -0
  63. package/edge/components/gInput.vue +1331 -0
  64. package/edge/components/loggingIn.vue +41 -0
  65. package/edge/components/menu.vue +137 -0
  66. package/edge/components/menuContent.vue +132 -0
  67. package/edge/components/myAccount.vue +317 -0
  68. package/edge/components/myOrganizations.vue +75 -0
  69. package/edge/components/myProfile.vue +122 -0
  70. package/edge/components/orgSwitcher.vue +25 -0
  71. package/edge/components/organizationMembers.vue +522 -0
  72. package/edge/components/organizationSettings.vue +271 -0
  73. package/edge/components/shad/breadcrumbs.vue +35 -0
  74. package/edge/components/shad/button.vue +43 -0
  75. package/edge/components/shad/checkbox.vue +73 -0
  76. package/edge/components/shad/combobox.vue +238 -0
  77. package/edge/components/shad/datepicker.vue +184 -0
  78. package/edge/components/shad/dialog.vue +32 -0
  79. package/edge/components/shad/dropdownMenu.vue +54 -0
  80. package/edge/components/shad/dropdownMenuItem.vue +21 -0
  81. package/edge/components/shad/form.vue +59 -0
  82. package/edge/components/shad/html.vue +877 -0
  83. package/edge/components/shad/input.vue +139 -0
  84. package/edge/components/shad/number.vue +109 -0
  85. package/edge/components/shad/select.vue +151 -0
  86. package/edge/components/shad/selectTags.vue +278 -0
  87. package/edge/components/shad/switch.vue +67 -0
  88. package/edge/components/shad/tags.vue +137 -0
  89. package/edge/components/shad/textarea.vue +102 -0
  90. package/edge/components/shad/typeMoney.vue +167 -0
  91. package/edge/components/sideBar.vue +288 -0
  92. package/edge/components/sideBarContent.vue +268 -0
  93. package/edge/components/sidebarProvider.vue +33 -0
  94. package/edge/components/tooltip.vue +16 -0
  95. package/edge/components/userMenu.vue +148 -0
  96. package/edge/components/v/alert.vue +59 -0
  97. package/edge/components/v/alertTitle.vue +18 -0
  98. package/edge/components/v/card.vue +53 -0
  99. package/edge/components/v/cardActions.vue +18 -0
  100. package/edge/components/v/cardText.vue +18 -0
  101. package/edge/components/v/cardTitle.vue +20 -0
  102. package/edge/components/v/col.vue +56 -0
  103. package/edge/components/v/list.vue +46 -0
  104. package/edge/components/v/listItem.vue +26 -0
  105. package/edge/components/v/listItemTitle.vue +18 -0
  106. package/edge/components/v/row.vue +42 -0
  107. package/edge/components/v/toolbar.vue +24 -0
  108. package/edge/composables/global.ts +519 -0
  109. package/edge-pull.sh +2 -0
  110. package/edge-push.sh +1 -0
  111. package/edge-status.sh +14 -0
  112. package/firebase.json +5 -2
  113. package/firebase_init.sh +21 -6
  114. package/package.json +1 -1
  115. package/plugins/firebase.client.ts +1 -0
  116. package/edge-components-install.sh +0 -1
@@ -0,0 +1,1298 @@
1
+ <script setup lang="js">
2
+ import { toTypedSchema } from '@vee-validate/zod'
3
+ import * as z from 'zod'
4
+
5
+ import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Loader2, MoreHorizontal } from 'lucide-vue-next'
6
+ const props = defineProps({
7
+ site: {
8
+ type: String,
9
+ required: true,
10
+ },
11
+ page: {
12
+ type: String,
13
+ required: false,
14
+ default: '',
15
+ },
16
+ })
17
+ const edgeFirebase = inject('edgeFirebase')
18
+
19
+ const normalizeForCompare = (value) => {
20
+ if (Array.isArray(value))
21
+ return value.map(normalizeForCompare)
22
+ if (value && typeof value === 'object') {
23
+ return Object.keys(value).sort().reduce((acc, key) => {
24
+ acc[key] = normalizeForCompare(value[key])
25
+ return acc
26
+ }, {})
27
+ }
28
+ return value
29
+ }
30
+
31
+ const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
32
+ const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
33
+
34
+ const isTemplateSite = computed(() => props.site === 'templates')
35
+ const router = useRouter()
36
+
37
+ const state = reactive({
38
+ filter: '',
39
+ userFilter: 'all',
40
+ newDocs: {
41
+ sites: {
42
+ name: { bindings: { 'field-type': 'text', 'label': 'Name' }, cols: '12', value: '' },
43
+ theme: { bindings: { 'field-type': 'collection', 'label': 'Themes', 'collection-path': 'themes' }, cols: '12', value: '' },
44
+ allowedThemes: { bindings: { 'field-type': 'tags', 'label': 'Allowed Themes' }, cols: '12', value: [] },
45
+ logo: { bindings: { 'field-type': 'text', 'label': 'Logo' }, cols: '12', value: '' },
46
+ favicon: { bindings: { 'field-type': 'text', 'label': 'Favicon' }, cols: '12', value: '' },
47
+ menuPosition: { bindings: { 'field-type': 'select', 'label': 'Menu Position', 'items': ['left', 'center', 'right'] }, cols: '12', value: 'right' },
48
+ domains: { bindings: { 'field-type': 'tags', 'label': 'Domains', 'helper': 'Add or remove domains' }, cols: '12', value: [] },
49
+ contactEmail: { bindings: { 'field-type': 'text', 'label': 'Contact Email' }, cols: '12', value: '' },
50
+ metaTitle: { bindings: { 'field-type': 'text', 'label': 'Meta Title' }, cols: '12', value: '' },
51
+ metaDescription: { bindings: { 'field-type': 'textarea', 'label': 'Meta Description' }, cols: '12', value: '' },
52
+ structuredData: { bindings: { 'field-type': 'textarea', 'label': 'Structured Data (JSON-LD)' }, cols: '12', value: '' },
53
+ users: { bindings: { 'field-type': 'users', 'label': 'Users', 'hint': 'Choose users' }, cols: '12', value: [] },
54
+ aiAgentUserId: { bindings: { 'field-type': 'select', 'label': 'Agent Data for AI to use to build initial site' }, cols: '12', value: '' },
55
+ aiInstructions: { bindings: { 'field-type': 'textarea', 'label': 'Additional AI Instructions' }, cols: '12', value: '' },
56
+ },
57
+ },
58
+ mounted: false,
59
+ page: {},
60
+ menus: { 'Site Root': [], 'Not In Menu': [] },
61
+ saving: false,
62
+ siteSettings: false,
63
+ hasError: false,
64
+ updating: false,
65
+ logoPickerOpen: false,
66
+ faviconPickerOpen: false,
67
+ aiSectionOpen: false,
68
+ selectedPostId: '',
69
+ viewMode: 'pages',
70
+ })
71
+
72
+ const pageInit = {
73
+ name: '',
74
+ content: [],
75
+ blockIds: [],
76
+ }
77
+
78
+ const schemas = {
79
+ sites: toTypedSchema(z.object({
80
+ name: z.string({
81
+ required_error: 'Name is required',
82
+ }).min(1, { message: 'Name is required' }),
83
+ domains: z
84
+ .array(z.string().max(45, 'Each domain must be 45 characters or fewer'))
85
+ .refine(arr => !!(arr && arr[0] && String(arr[0]).trim().length), {
86
+ message: 'At least one domain is required',
87
+ path: ['domains', 0],
88
+ }),
89
+ contactEmail: z.string().optional(),
90
+ theme: z.string({
91
+ required_error: 'Theme is required',
92
+ }).min(1, { message: 'Theme is required' }),
93
+ allowedThemes: z.array(z.string()).optional(),
94
+ logo: z.string().optional(),
95
+ favicon: z.string().optional(),
96
+ menuPosition: z.enum(['left', 'center', 'right']).optional(),
97
+ metaTitle: z.string().optional(),
98
+ metaDescription: z.string().optional(),
99
+ structuredData: z.string().optional(),
100
+ aiAgentUserId: z.string().optional(),
101
+ aiInstructions: z.string().optional(),
102
+ })),
103
+ pages: toTypedSchema(z.object({
104
+ name: z.string({
105
+ required_error: 'Name is required',
106
+ }).min(1, { message: 'Name is required' }),
107
+ })),
108
+ }
109
+
110
+ const isAdmin = computed(() => {
111
+ return edgeGlobal.isAdminGlobal(edgeFirebase).value
112
+ })
113
+
114
+ const siteData = computed(() => {
115
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site] || {}
116
+ })
117
+
118
+ const themeCollection = computed(() => {
119
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {}
120
+ })
121
+
122
+ const deriveThemeLabel = (doc = {}) => {
123
+ return doc?.name
124
+ || doc?.title
125
+ || doc?.theme?.name
126
+ || doc?.theme?.title
127
+ || doc?.meta?.name
128
+ || doc?.meta?.title
129
+ || ''
130
+ }
131
+
132
+ const themeOptions = computed(() => {
133
+ return Object.entries(themeCollection.value)
134
+ .map(([value, doc]) => ({
135
+ value,
136
+ label: deriveThemeLabel(doc) || value,
137
+ }))
138
+ .sort((a, b) => a.label.localeCompare(b.label))
139
+ })
140
+
141
+ const themeOptionsMap = computed(() => {
142
+ const map = new Map()
143
+ for (const option of themeOptions.value) {
144
+ map.set(option.value, option)
145
+ }
146
+ return map
147
+ })
148
+
149
+ const orgUsers = computed(() => edgeFirebase.state?.users || {})
150
+ const userOptions = computed(() => {
151
+ return Object.entries(orgUsers.value || {})
152
+ .map(([id, user]) => ({
153
+ value: user?.userId || id,
154
+ label: user?.meta?.name || user?.userId || id,
155
+ }))
156
+ .sort((a, b) => a.label.localeCompare(b.label))
157
+ })
158
+
159
+ const themeItemsForAllowed = (allowed, current) => {
160
+ const base = themeOptions.value
161
+ const allowedList = Array.isArray(allowed) ? allowed.filter(Boolean) : []
162
+ if (allowedList.length) {
163
+ const allowedSet = new Set(allowedList)
164
+ const filtered = base.filter(option => allowedSet.has(option.value))
165
+ if (current && !allowedSet.has(current)) {
166
+ const currentOption = themeOptionsMap.value.get(current)
167
+ if (currentOption)
168
+ filtered.push(currentOption)
169
+ }
170
+ return filtered
171
+ }
172
+
173
+ if (current) {
174
+ const currentOption = themeOptionsMap.value.get(current)
175
+ return currentOption ? [currentOption] : []
176
+ }
177
+
178
+ return []
179
+ }
180
+
181
+ const menuPositionOptions = [
182
+ { value: 'left', label: 'Left' },
183
+ { value: 'center', label: 'Center' },
184
+ { value: 'right', label: 'Right' },
185
+ ]
186
+
187
+ const TEMPLATE_PAGES_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
188
+ const seededSiteIds = new Set()
189
+
190
+ const slugify = (value) => {
191
+ return String(value || '')
192
+ .trim()
193
+ .toLowerCase()
194
+ .replace(/[^a-z0-9]+/g, '-')
195
+ .replace(/(^-|-$)+/g, '')
196
+ }
197
+
198
+ const titleFromSlug = (slug) => {
199
+ return slug
200
+ .split(/[-_]/)
201
+ .filter(Boolean)
202
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
203
+ .join(' ') || 'New Page'
204
+ }
205
+
206
+ const ensureMenuBuckets = (menus) => {
207
+ const normalized = (menus && typeof menus === 'object')
208
+ ? edgeGlobal.dupObject(menus)
209
+ : {}
210
+ if (!Array.isArray(normalized['Site Root']))
211
+ normalized['Site Root'] = []
212
+ if (!Array.isArray(normalized['Not In Menu']))
213
+ normalized['Not In Menu'] = []
214
+ return normalized
215
+ }
216
+
217
+ const ensureUniqueSlug = (candidate, templateDoc, usedSlugs) => {
218
+ const fallbackBase = slugify(templateDoc?.slug || templateDoc?.name || '')
219
+ let base = (candidate && candidate.trim().length) ? slugify(candidate) : ''
220
+ if (!base)
221
+ base = fallbackBase || `page-${usedSlugs.size + 1}`
222
+ let slugCandidate = base
223
+ let suffix = 2
224
+ while (usedSlugs.has(slugCandidate)) {
225
+ slugCandidate = `${base}-${suffix}`
226
+ suffix += 1
227
+ }
228
+ usedSlugs.add(slugCandidate)
229
+ return slugCandidate
230
+ }
231
+
232
+ const cloneBlocks = (blocks = []) => {
233
+ return Array.isArray(blocks) ? JSON.parse(JSON.stringify(blocks)) : []
234
+ }
235
+
236
+ const deriveBlockIdsFromDoc = (doc = {}) => {
237
+ const collectBlocks = (blocks) => {
238
+ if (!Array.isArray(blocks))
239
+ return []
240
+ return blocks
241
+ .map(block => block?.blockId)
242
+ .filter(Boolean)
243
+ }
244
+
245
+ const collectFromStructure = (structure) => {
246
+ if (!Array.isArray(structure))
247
+ return []
248
+ const ids = []
249
+ for (const row of structure) {
250
+ for (const column of row?.columns || []) {
251
+ if (Array.isArray(column?.blocks))
252
+ ids.push(...column.blocks.filter(Boolean))
253
+ }
254
+ }
255
+ return ids
256
+ }
257
+
258
+ const ids = new Set([
259
+ ...collectBlocks(doc.content),
260
+ ...collectBlocks(doc.postContent),
261
+ ...collectFromStructure(doc.structure),
262
+ ...collectFromStructure(doc.postStructure),
263
+ ])
264
+ return Array.from(ids)
265
+ }
266
+
267
+ const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') => {
268
+ const timestamp = Date.now()
269
+ const payload = {
270
+ name: displayName?.trim()?.length ? displayName : titleFromSlug(slug),
271
+ slug,
272
+ post: templateDoc?.post || false,
273
+ content: cloneBlocks(templateDoc?.content),
274
+ postContent: cloneBlocks(templateDoc?.postContent),
275
+ structure: cloneBlocks(templateDoc?.structure),
276
+ postStructure: cloneBlocks(templateDoc?.postStructure),
277
+ blockIds: [],
278
+ metaTitle: templateDoc?.metaTitle || '',
279
+ metaDescription: templateDoc?.metaDescription || '',
280
+ structuredData: templateDoc?.structuredData || '',
281
+ doc_created_at: timestamp,
282
+ last_updated: timestamp,
283
+ }
284
+ payload.blockIds = deriveBlockIdsFromDoc(payload)
285
+ return payload
286
+ }
287
+
288
+ const buildMenusFromDefaultPages = (defaultPages = []) => {
289
+ if (!Array.isArray(defaultPages) || !defaultPages.length)
290
+ return null
291
+ const menus = { 'Site Root': [], 'Not In Menu': [] }
292
+ const usedSlugs = new Set()
293
+ for (const entry of defaultPages) {
294
+ if (!entry?.pageId)
295
+ continue
296
+ const slug = ensureUniqueSlug(entry?.name || '', null, usedSlugs)
297
+ menus['Site Root'].push({
298
+ name: slug,
299
+ item: entry.pageId,
300
+ })
301
+ }
302
+ return menus
303
+ }
304
+
305
+ const deriveThemeMenus = (themeDoc = {}) => {
306
+ if (themeDoc?.defaultMenus && Object.keys(themeDoc.defaultMenus || {}).length)
307
+ return ensureMenuBuckets(themeDoc.defaultMenus)
308
+ if (Array.isArray(themeDoc?.defaultPages) && themeDoc.defaultPages.length)
309
+ return buildMenusFromDefaultPages(themeDoc.defaultPages)
310
+ return null
311
+ }
312
+
313
+ const ensureTemplatePagesSnapshot = async () => {
314
+ if (!edgeFirebase.data?.[TEMPLATE_PAGES_PATH.value])
315
+ await edgeFirebase.startSnapshot(TEMPLATE_PAGES_PATH.value)
316
+ return edgeFirebase.data?.[TEMPLATE_PAGES_PATH.value] || {}
317
+ }
318
+
319
+ const duplicateEntriesWithPages = async (entries = [], options) => {
320
+ const {
321
+ templatePages,
322
+ siteId,
323
+ usedSlugs,
324
+ } = options
325
+ const next = []
326
+ for (const entry of entries) {
327
+ if (!entry || entry.item == null)
328
+ continue
329
+ if (typeof entry.item === 'string' || entry.item === '') {
330
+ const templateDoc = templatePages?.[entry.item] || null
331
+ const slug = ensureUniqueSlug(entry.name || '', templateDoc, usedSlugs)
332
+ const payload = buildPagePayloadFromTemplateDoc(templateDoc, slug, entry.name || '')
333
+ try {
334
+ const result = await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${siteId}/pages`, payload)
335
+ const docId = result?.meta?.docId
336
+ if (docId) {
337
+ next.push({
338
+ ...entry,
339
+ name: slug,
340
+ item: docId,
341
+ })
342
+ }
343
+ }
344
+ catch (error) {
345
+ console.error('Failed to duplicate template page for site seed', error)
346
+ }
347
+ }
348
+ else if (typeof entry.item === 'object') {
349
+ const folderName = Object.keys(entry.item || {})[0]
350
+ if (!folderName)
351
+ continue
352
+ const children = await duplicateEntriesWithPages(entry.item[folderName], options)
353
+ if (children.length) {
354
+ next.push({
355
+ ...entry,
356
+ item: {
357
+ [folderName]: children,
358
+ },
359
+ })
360
+ }
361
+ }
362
+ }
363
+ return next
364
+ }
365
+
366
+ const seedNewSiteFromTheme = async (siteId, themeId) => {
367
+ if (!siteId || !themeId)
368
+ return
369
+ const themeDoc = themeCollection.value?.[themeId]
370
+ if (!themeDoc)
371
+ return
372
+ const themeMenus = deriveThemeMenus(themeDoc)
373
+ if (!themeMenus)
374
+ return
375
+ const templatePages = await ensureTemplatePagesSnapshot()
376
+ const usedSlugs = new Set()
377
+ const seededMenus = ensureMenuBuckets(themeMenus)
378
+ seededMenus['Site Root'] = await duplicateEntriesWithPages(seededMenus['Site Root'], { templatePages, siteId, usedSlugs })
379
+ seededMenus['Not In Menu'] = await duplicateEntriesWithPages(seededMenus['Not In Menu'], { templatePages, siteId, usedSlugs })
380
+ await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites`, siteId, { menus: seededMenus })
381
+ }
382
+
383
+ const handleNewSiteSaved = async ({ docId, data, collection }) => {
384
+ if (props.site !== 'new')
385
+ return
386
+ if (collection !== 'sites')
387
+ return
388
+ if (!docId || seededSiteIds.has(docId))
389
+ return
390
+ const themeId = data?.theme
391
+ if (!themeId)
392
+ return
393
+ seededSiteIds.add(docId)
394
+ try {
395
+ await seedNewSiteFromTheme(docId, themeId)
396
+ }
397
+ catch (error) {
398
+ console.error('Failed to seed site from theme defaults', error)
399
+ seededSiteIds.delete(docId)
400
+ }
401
+ }
402
+
403
+ onBeforeMount(async () => {
404
+ if (!edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/users`]) {
405
+ await edgeFirebase.startUsersSnapshot(edgeGlobal.edgeState.organizationDocPath)
406
+ }
407
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/published-site-settings`]) {
408
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/published-site-settings`)
409
+ }
410
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/pages`]) {
411
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/pages`)
412
+ }
413
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]) {
414
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`)
415
+ }
416
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published`]) {
417
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published`)
418
+ }
419
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`]) {
420
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`)
421
+ }
422
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/posts`]) {
423
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/posts`)
424
+ }
425
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`]) {
426
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`)
427
+ }
428
+ state.mounted = true
429
+ })
430
+
431
+ const isSiteDiff = computed(() => {
432
+ const publishedSite = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`]?.[props.site]
433
+ if (!publishedSite && siteData.value) {
434
+ return true
435
+ }
436
+ if (publishedSite && !siteData.value) {
437
+ return true
438
+ }
439
+ if (publishedSite && siteData.value) {
440
+ return !areEqualNormalized({
441
+ domains: publishedSite.domains,
442
+ menus: publishedSite.menus,
443
+ theme: publishedSite.theme,
444
+ allowedThemes: publishedSite.allowedThemes,
445
+ logo: publishedSite.logo,
446
+ favicon: publishedSite.favicon,
447
+ menuPosition: publishedSite.menuPosition,
448
+ contactEmail: publishedSite.contactEmail,
449
+ metaTitle: publishedSite.metaTitle,
450
+ metaDescription: publishedSite.metaDescription,
451
+ structuredData: publishedSite.structuredData,
452
+ }, {
453
+ domains: siteData.value.domains,
454
+ menus: siteData.value.menus,
455
+ theme: siteData.value.theme,
456
+ allowedThemes: siteData.value.allowedThemes,
457
+ logo: siteData.value.logo,
458
+ favicon: siteData.value.favicon,
459
+ menuPosition: siteData.value.menuPosition,
460
+ contactEmail: siteData.value.contactEmail,
461
+ metaTitle: siteData.value.metaTitle,
462
+ metaDescription: siteData.value.metaDescription,
463
+ structuredData: siteData.value.structuredData,
464
+ })
465
+ }
466
+ return false
467
+ })
468
+
469
+ const publishSiteSettings = async () => {
470
+ console.log('Publishing site settings for site:', props.site)
471
+ await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`, siteData.value)
472
+ }
473
+
474
+ const discardSiteSettings = async () => {
475
+ console.log('Discarding site settings for site:', props.site)
476
+ const publishedSite = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`]?.[props.site]
477
+ if (publishedSite) {
478
+ await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites`, props.site, {
479
+ domains: publishedSite.domains || [],
480
+ menus: publishedSite.menus || {},
481
+ theme: publishedSite.theme || '',
482
+ allowedThemes: publishedSite.allowedThemes || [],
483
+ logo: publishedSite.logo || '',
484
+ favicon: publishedSite.favicon || '',
485
+ menuPosition: publishedSite.menuPosition || '',
486
+ contactEmail: publishedSite.contactEmail || '',
487
+ metaTitle: publishedSite.metaTitle || '',
488
+ metaDescription: publishedSite.metaDescription || '',
489
+ structuredData: publishedSite.structuredData || '',
490
+ })
491
+ }
492
+ }
493
+
494
+ const unPublishSite = async () => {
495
+ console.log('Unpublishing site:', props.site)
496
+ const pages = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
497
+ for (const pageId of Object.keys(pages)) {
498
+ await edgeFirebase.removeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageId)
499
+ }
500
+ await edgeFirebase.removeDoc(`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`, props.site)
501
+ }
502
+
503
+ const publishSite = async () => {
504
+ for (const [pageId, pageData] of Object.entries(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {})) {
505
+ await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageData)
506
+ }
507
+ }
508
+
509
+ const pages = computed(() => {
510
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
511
+ })
512
+
513
+ const publishedPages = computed(() => {
514
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`] || {}
515
+ })
516
+
517
+ const pageRouteBase = computed(() => {
518
+ return props.site === 'templates'
519
+ ? '/app/dashboard/templates'
520
+ : `/app/dashboard/sites/${props.site}`
521
+ })
522
+
523
+ const pageList = computed(() => {
524
+ return Object.entries(pages.value || {})
525
+ .map(([id, data]) => ({
526
+ id,
527
+ name: data?.name || 'Untitled Page',
528
+ lastUpdated: data?.last_updated || data?.doc_created_at,
529
+ }))
530
+ .sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0))
531
+ })
532
+
533
+ const formatTimestamp = (input) => {
534
+ if (!input)
535
+ return 'Not yet saved'
536
+ try {
537
+ return new Date(input).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })
538
+ }
539
+ catch {
540
+ return 'Not yet saved'
541
+ }
542
+ }
543
+
544
+ const isPublishedPageDiff = (pageId) => {
545
+ const publishedPage = publishedPages.value?.[pageId]
546
+ const draftPage = pages.value?.[pageId]
547
+ if (!publishedPage && draftPage) {
548
+ return true
549
+ }
550
+ if (publishedPage && !draftPage) {
551
+ return true
552
+ }
553
+ if (publishedPage && draftPage) {
554
+ return !areEqualNormalized(
555
+ {
556
+ content: publishedPage.content,
557
+ postContent: publishedPage.postContent,
558
+ structure: publishedPage.structure,
559
+ postStructure: publishedPage.postStructure,
560
+ metaTitle: publishedPage.metaTitle,
561
+ metaDescription: publishedPage.metaDescription,
562
+ structuredData: publishedPage.structuredData,
563
+ },
564
+ {
565
+ content: draftPage.content,
566
+ postContent: draftPage.postContent,
567
+ structure: draftPage.structure,
568
+ postStructure: draftPage.postStructure,
569
+ metaTitle: draftPage.metaTitle,
570
+ metaDescription: draftPage.metaDescription,
571
+ structuredData: draftPage.structuredData,
572
+ },
573
+ )
574
+ }
575
+ return false
576
+ }
577
+
578
+ const pageStatusLabel = pageId => (isPublishedPageDiff(pageId) ? 'Draft' : 'Published')
579
+ const hasSelection = computed(() => Boolean(props.page) || Boolean(state.selectedPostId))
580
+ const showSplitView = computed(() => isTemplateSite.value || state.viewMode === 'pages' || hasSelection.value)
581
+ const isEditingPost = computed(() => state.viewMode === 'posts' && Boolean(state.selectedPostId))
582
+
583
+ const setViewMode = (mode) => {
584
+ if (state.viewMode === mode)
585
+ return
586
+ state.viewMode = mode
587
+ state.selectedPostId = ''
588
+ if (props.page)
589
+ router.replace(pageRouteBase.value)
590
+ }
591
+
592
+ const handlePostSelect = (postId) => {
593
+ if (!postId)
594
+ return
595
+ state.selectedPostId = postId
596
+ state.viewMode = 'posts'
597
+ if (props.page)
598
+ router.replace(pageRouteBase.value)
599
+ }
600
+
601
+ const clearPostSelection = () => {
602
+ state.selectedPostId = ''
603
+ }
604
+
605
+ watch (() => siteData.value, () => {
606
+ if (isTemplateSite.value)
607
+ return
608
+ if (siteData.value?.menus) {
609
+ console.log('Loading menus from site data')
610
+ state.saving = true
611
+ state.menus = JSON.parse(JSON.stringify(siteData.value.menus))
612
+ state.saving = false
613
+ }
614
+ }, { immediate: true, deep: true })
615
+
616
+ const buildTemplateMenus = (pagesCollection) => {
617
+ const items = Object.entries(pagesCollection || {})
618
+ .map(([id, doc]) => ({
619
+ name: doc?.name || 'Untitled Page',
620
+ item: id,
621
+ }))
622
+ .sort((a, b) => a.name.localeCompare(b.name))
623
+ return {
624
+ 'Site Root': items,
625
+ }
626
+ }
627
+
628
+ watch(pages, (pagesCollection) => {
629
+ if (!isTemplateSite.value)
630
+ return
631
+ const nextMenu = buildTemplateMenus(pagesCollection)
632
+ if (areEqualNormalized(state.menus, nextMenu))
633
+ return
634
+ state.menus = nextMenu
635
+ }, { immediate: true, deep: true })
636
+
637
+ watch(() => state.siteSettings, (open) => {
638
+ if (!open)
639
+ state.logoPickerOpen = false
640
+ if (!open)
641
+ state.faviconPickerOpen = false
642
+ })
643
+
644
+ watch(() => props.page, (next) => {
645
+ if (next) {
646
+ state.selectedPostId = ''
647
+ state.viewMode = 'pages'
648
+ return
649
+ }
650
+ if (state.selectedPostId) {
651
+ state.viewMode = 'posts'
652
+ }
653
+ })
654
+
655
+ watch(() => state.menus, async (newVal) => {
656
+ if (areEqualNormalized(siteData.value.menus, newVal)) {
657
+ return
658
+ }
659
+ if (!state.mounted) {
660
+ return
661
+ }
662
+ if (state.saving) {
663
+ return
664
+ }
665
+ state.saving = true
666
+ // todo loop through menus and if any item is a blank string use the name {name:'blah', item: ''} and used edgeFirebase to add that page and wait for complete and put docId as value of item
667
+ const newPage = JSON.parse(JSON.stringify(pageInit))
668
+ for (const [menuName, items] of Object.entries(newVal)) {
669
+ for (const [index, item] of items.entries()) {
670
+ if (typeof item.item === 'string') {
671
+ if (item.item === '') {
672
+ newPage.name = item.name
673
+ console.log('Creating new page for menu item:', item)
674
+ const result = await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, newPage)
675
+ const docId = result?.meta?.docId
676
+ item.item = docId
677
+ }
678
+ else {
679
+ if (item.name === 'Deleting...') {
680
+ await edgeFirebase.removeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, item.item)
681
+ state.menus[menuName].splice(index, 1)
682
+ }
683
+ }
684
+ }
685
+ if (typeof item.item === 'object') {
686
+ for (const [subMenuName, subItems] of Object.entries(item.item)) {
687
+ for (const [subIndex, subItem] of subItems.entries()) {
688
+ if (typeof subItem.item === 'string') {
689
+ if (subItem.item === '') {
690
+ newPage.name = subItem.name
691
+ const result = await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, newPage)
692
+ const docId = result?.meta?.docId
693
+ subItem.item = docId
694
+ }
695
+ else {
696
+ if (subItem.name === 'Deleting...') {
697
+ await edgeFirebase.removeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, subItem.item)
698
+ state.menus[menuName][index].item[subMenuName].splice(subIndex, 1)
699
+ }
700
+ }
701
+ }
702
+ }
703
+ }
704
+ if (Object.keys(item.item).length === 0) {
705
+ state.menus[menuName].splice(index, 1)
706
+ }
707
+ }
708
+ }
709
+ }
710
+ if (!isTemplateSite.value)
711
+ await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites`, props.site, { menus: state.menus })
712
+ state.saving = false
713
+ }, { deep: true })
714
+
715
+ const formErrors = (error) => {
716
+ console.log('Form errors:', error)
717
+ console.log(Object.values(error))
718
+ if (Object.values(error).length > 0) {
719
+ console.log('Form errors found')
720
+ state.hasError = true
721
+ console.log(state.hasError)
722
+ }
723
+ state.hasError = false
724
+ }
725
+
726
+ const onSubmit = () => {
727
+ if (!state.hasError) {
728
+ state.siteSettings = false
729
+ }
730
+ }
731
+
732
+ const isAllPagesPublished = computed(() => {
733
+ const pagesData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
734
+ const publishedData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`] || {}
735
+ return Object.keys(pagesData).length === Object.keys(publishedData).length
736
+ })
737
+
738
+ const isSiteSettingPublished = computed(() => {
739
+ const publishedSite = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`]?.[props.site]
740
+ return !!publishedSite
741
+ })
742
+
743
+ const isAnyPagesDiff = computed(() => {
744
+ const pagesData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
745
+ const publishedData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`] || {}
746
+ for (const [pageId, pageData] of Object.entries(pagesData)) {
747
+ const publishedPage = publishedData?.[pageId]
748
+ if (!publishedPage) {
749
+ return true
750
+ }
751
+ if (!areEqualNormalized(
752
+ {
753
+ content: pageData.content,
754
+ postContent: pageData.postContent,
755
+ structure: pageData.structure,
756
+ postStructure: pageData.postStructure,
757
+ metaTitle: pageData.metaTitle,
758
+ metaDescription: pageData.metaDescription,
759
+ structuredData: pageData.structuredData,
760
+ },
761
+ {
762
+ content: publishedPage.content,
763
+ postContent: publishedPage.postContent,
764
+ structure: publishedPage.structure,
765
+ postStructure: publishedPage.postStructure,
766
+ metaTitle: publishedPage.metaTitle,
767
+ metaDescription: publishedPage.metaDescription,
768
+ structuredData: publishedPage.structuredData,
769
+ },
770
+ )) {
771
+ return true
772
+ }
773
+ }
774
+ return false
775
+ })
776
+
777
+ const isAnyPagesPublished = computed(() => {
778
+ const publishedData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`] || {}
779
+ return Object.keys(publishedData).length > 0
780
+ })
781
+
782
+ const pageSettingsUpdated = async (pageData) => {
783
+ console.log('Page settings updated:', pageData)
784
+ state.updating = true
785
+ await nextTick()
786
+ state.updating = false
787
+ }
788
+ </script>
789
+
790
+ <template>
791
+ <div
792
+ v-if="edgeGlobal.edgeState.organizationDocPath"
793
+ >
794
+ <edge-editor
795
+ v-if="!props.page && props.site === 'new'"
796
+ collection="sites"
797
+ :doc-id="props.site"
798
+ :schema="schemas.sites"
799
+ :new-doc-schema="state.newDocs.sites"
800
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none"
801
+ :show-footer="false"
802
+ @saved="handleNewSiteSaved"
803
+ >
804
+ <template #header-start="slotProps">
805
+ <FilePenLine class="mr-2" />
806
+ {{ slotProps.title }}
807
+ </template>
808
+ <template #header-end="slotProps">
809
+ <edge-shad-button
810
+ v-if="!slotProps.unsavedChanges"
811
+ to="/app/dashboard/sites"
812
+ class="bg-red-700 uppercase h-8 hover:bg-slate-400 w-20"
813
+ >
814
+ Close
815
+ </edge-shad-button>
816
+ <edge-shad-button
817
+ v-else
818
+ to="/app/dashboard/sites"
819
+ class="bg-red-700 uppercase h-8 hover:bg-slate-400 w-20"
820
+ >
821
+ Cancel
822
+ </edge-shad-button>
823
+ <edge-shad-button
824
+ type="submit"
825
+ class="bg-slate-500 uppercase h-8 hover:bg-slate-400 w-20"
826
+ >
827
+ Save
828
+ </edge-shad-button>
829
+ </template>
830
+ <template #main="slotProps">
831
+ <div class="flex-col flex gap-4 mt-4">
832
+ <edge-shad-input
833
+ v-model="slotProps.workingDoc.name"
834
+ name="name"
835
+ label="Name"
836
+ placeholder="Enter name"
837
+ class="w-full"
838
+ />
839
+ <edge-shad-tags
840
+ v-model="slotProps.workingDoc.domains"
841
+ name="domains"
842
+ label="Domains"
843
+ placeholder="Add or remove domains"
844
+ class="w-full"
845
+ />
846
+ <edge-shad-select-tags
847
+ v-if="isAdmin"
848
+ :model-value="Array.isArray(slotProps.workingDoc.allowedThemes) ? slotProps.workingDoc.allowedThemes : []"
849
+ name="allowedThemes"
850
+ label="Allowed Themes"
851
+ placeholder="Select allowed themes"
852
+ class="w-full"
853
+ :items="themeOptions"
854
+ item-title="label"
855
+ item-value="value"
856
+ @update:model-value="(value) => {
857
+ const normalized = Array.isArray(value) ? value : []
858
+ slotProps.workingDoc.allowedThemes = normalized
859
+ if (normalized.length && !normalized.includes(slotProps.workingDoc.theme)) {
860
+ slotProps.workingDoc.theme = normalized[0] || ''
861
+ }
862
+ }"
863
+ />
864
+ <edge-shad-select
865
+ :model-value="slotProps.workingDoc.theme || ''"
866
+ name="theme"
867
+ label="Theme"
868
+ placeholder="Select a theme"
869
+ class="w-full"
870
+ :items="themeItemsForAllowed(isAdmin ? slotProps.workingDoc.allowedThemes : themeOptions.map(option => option.value), slotProps.workingDoc.theme)"
871
+ item-title="label"
872
+ item-value="value"
873
+ @update:model-value="value => (slotProps.workingDoc.theme = value || '')"
874
+ />
875
+ <edge-shad-select-tags
876
+ v-if="Object.keys(orgUsers).length > 0"
877
+ v-model="slotProps.workingDoc.users" :disabled="!edgeGlobal.isAdminGlobal(edgeFirebase).value"
878
+ :items="userOptions"
879
+ name="users"
880
+ label="Users"
881
+ item-title="label"
882
+ item-value="value"
883
+ placeholder="Select users"
884
+ class="w-full"
885
+ :multiple="true"
886
+ />
887
+ <div class="rounded-lg border border-dashed border-slate-200 p-4 ">
888
+ <div class="flex items-start justify-between gap-3">
889
+ <div>
890
+ <div class="text-sm font-semibold text-foreground">
891
+ AI (optional)
892
+ </div>
893
+ <p class="text-xs text-muted-foreground">
894
+ Include user data and instructions for the first AI-generated version of the site.
895
+ </p>
896
+ </div>
897
+ <!-- <edge-shad-switch
898
+ v-model="state.aiSectionOpen"
899
+ name="enableAi"
900
+ label="Add AI details"
901
+ /> -->
902
+ </div>
903
+ <div class="space-y-3">
904
+ <edge-shad-select
905
+ :model-value="slotProps.workingDoc.aiAgentUserId || ''"
906
+ name="aiAgentUserId"
907
+ label="User Data for AI to use to build initial site"
908
+ placeholder="- select one -"
909
+ class="w-full"
910
+ :items="userOptions"
911
+ item-title="label"
912
+ item-value="value"
913
+ @update:model-value="value => (slotProps.workingDoc.aiAgentUserId = value || '')"
914
+ />
915
+ <edge-shad-textarea
916
+ v-model="slotProps.workingDoc.aiInstructions"
917
+ name="aiInstructions"
918
+ label="Additional AI instructions"
919
+ placeholder="Share any goals, tone, or details the AI should prioritize"
920
+ class="w-full"
921
+ />
922
+ </div>
923
+ </div>
924
+ </div>
925
+ </template>
926
+ </edge-editor>
927
+ <div v-else class="flex flex-col h-[calc(100vh-58px)] overflow-hidden">
928
+ <div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3 px-4 py-2 border-b bg-secondary">
929
+ <div class="flex items-center gap-3">
930
+ <FileStack class="w-5 h-5" />
931
+ <span class="text-lg font-semibold">
932
+ {{ siteData.name || 'Templates' }}
933
+ </span>
934
+ </div>
935
+ <div class="flex justify-center">
936
+ <div v-if="!isTemplateSite" class="flex items-center rounded-full border border-border bg-background p-1 shadow-sm">
937
+ <edge-shad-button
938
+ variant="ghost"
939
+ size="sm"
940
+ class="h-8 px-4 text-xs gap-2 rounded-full"
941
+ :class="state.viewMode === 'pages' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
942
+ @click="setViewMode('pages')"
943
+ >
944
+ <FileStack class="h-4 w-4" />
945
+ Pages
946
+ </edge-shad-button>
947
+ <edge-shad-button
948
+ variant="ghost"
949
+ size="sm"
950
+ class="h-8 px-4 text-xs gap-2 rounded-full"
951
+ :class="state.viewMode === 'posts' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
952
+ @click="setViewMode('posts')"
953
+ >
954
+ <FilePenLine class="h-4 w-4" />
955
+ Posts
956
+ </edge-shad-button>
957
+ </div>
958
+ </div>
959
+ <div v-if="!isTemplateSite" class="flex items-center gap-3 justify-end">
960
+ <Transition name="fade" mode="out-in">
961
+ <div v-if="isSiteDiff" key="unpublished" class="flex gap-1 items-center bg-yellow-100 text-xs py-1 px-3 text-yellow-800 rounded">
962
+ <CircleAlert class="!text-yellow-800 w-3 h-3" />
963
+ <span class="font-medium text-[10px]">
964
+ Unpublished Settings
965
+ </span>
966
+ </div>
967
+ <div v-else key="published" class="flex gap-1 items-center bg-green-100 text-xs py-1 px-3 text-green-800 rounded">
968
+ <FileCheck class="!text-green-800 w-3 h-3" />
969
+ <span class="font-medium text-[10px]">
970
+ Settings Published
971
+ </span>
972
+ </div>
973
+ </Transition>
974
+ <DropdownMenu>
975
+ <DropdownMenuTrigger as-child>
976
+ <edge-shad-button variant="outline" size="icon" class="h-9 w-9">
977
+ <MoreHorizontal />
978
+ </edge-shad-button>
979
+ </DropdownMenuTrigger>
980
+ <DropdownMenuContent side="right" align="start">
981
+ <DropdownMenuLabel class="flex items-center gap-2">
982
+ <FileStack class="w-5 h-5" />{{ siteData.name || 'Templates' }}
983
+ </DropdownMenuLabel>
984
+
985
+ <DropdownMenuSeparator v-if="isSiteDiff" />
986
+ <DropdownMenuLabel v-if="isSiteDiff" class="flex items-center gap-2">
987
+ Site Settings
988
+ </DropdownMenuLabel>
989
+
990
+ <DropdownMenuItem v-if="isSiteDiff" class="pl-4 text-xs" @click="publishSiteSettings">
991
+ <FolderUp />
992
+ Publish
993
+ </DropdownMenuItem>
994
+ <DropdownMenuItem v-if="isSiteDiff && isSiteSettingPublished" class="pl-4 text-xs" @click="discardSiteSettings">
995
+ <FolderX />
996
+ Discard Changes
997
+ </DropdownMenuItem>
998
+ <DropdownMenuSeparator />
999
+ <DropdownMenuItem v-if="isAnyPagesDiff" @click="publishSite">
1000
+ <FolderUp />
1001
+ Publish All Pages
1002
+ </DropdownMenuItem>
1003
+ <DropdownMenuItem v-if="isSiteSettingPublished || isAnyPagesPublished" @click="unPublishSite">
1004
+ <FolderDown />
1005
+ Unpublish Site
1006
+ </DropdownMenuItem>
1007
+
1008
+ <DropdownMenuItem @click="state.siteSettings = true">
1009
+ <FolderCog />
1010
+ <span>Settings</span>
1011
+ </DropdownMenuItem>
1012
+ </DropdownMenuContent>
1013
+ </DropdownMenu>
1014
+ </div>
1015
+ <div v-else />
1016
+ </div>
1017
+ <div class="flex-1">
1018
+ <Transition name="fade" mode="out-in">
1019
+ <div v-if="isEditingPost" class="w-full h-full">
1020
+ <edge-cms-posts
1021
+ mode="editor"
1022
+ :site="props.site"
1023
+ :selected-post-id="state.selectedPostId"
1024
+ @update:selected-post-id="clearPostSelection"
1025
+ />
1026
+ </div>
1027
+ <ResizablePanelGroup v-else-if="showSplitView" direction="horizontal" class="w-full h-full flex-1">
1028
+ <ResizablePanel class="bg-sidebar text-sidebar-foreground" :default-size="16">
1029
+ <SidebarGroup class="mt-0 pt-0">
1030
+ <SidebarGroupContent>
1031
+ <SidebarMenu>
1032
+ <template v-if="isTemplateSite || state.viewMode === 'pages'">
1033
+ <edge-cms-menu
1034
+ v-if="state.menus"
1035
+ v-model="state.menus"
1036
+ :site="props.site"
1037
+ :page="props.page"
1038
+ :is-template-site="isTemplateSite"
1039
+ :theme-options="themeOptions"
1040
+ @page-settings-update="pageSettingsUpdated"
1041
+ />
1042
+ </template>
1043
+ <template v-else>
1044
+ <edge-cms-posts
1045
+ mode="list"
1046
+ list-variant="sidebar"
1047
+ :site="props.site"
1048
+ @updating="isUpdating => state.updating = isUpdating"
1049
+ @update:selected-post-id="handlePostSelect"
1050
+ />
1051
+ </template>
1052
+ </SidebarMenu>
1053
+ </SidebarGroupContent>
1054
+ </SidebarGroup>
1055
+ </ResizablePanel>
1056
+ <ResizablePanel ref="mainPanel">
1057
+ <Transition name="fade" mode="out-in">
1058
+ <div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-50px)] overflow-y-auto w-full">
1059
+ <NuxtPage class="flex flex-col flex-1 px-0 mx-0 pt-0" />
1060
+ </div>
1061
+ <div v-else class="p-4 text-center flex text-slate-500 h-[calc(100vh-4rem)] justify-center items-center overflow-y-auto">
1062
+ <div class="text-4xl">
1063
+ <ArrowLeft class="inline-block w-12 h-12 mr-2" /> Select a page to get started.
1064
+ </div>
1065
+ </div>
1066
+ </Transition>
1067
+ </ResizablePanel>
1068
+ </ResizablePanelGroup>
1069
+ <div v-else class="flex-1 overflow-y-auto p-6">
1070
+ <div class="mx-auto w-full max-w-5xl space-y-6">
1071
+ <edge-cms-posts
1072
+ mode="list"
1073
+ list-variant="full"
1074
+ :site="props.site"
1075
+ @updating="isUpdating => state.updating = isUpdating"
1076
+ @update:selected-post-id="handlePostSelect"
1077
+ />
1078
+ </div>
1079
+ </div>
1080
+ </Transition>
1081
+ </div>
1082
+ </div>
1083
+ <Sheet v-model:open="state.siteSettings">
1084
+ <SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
1085
+ <SheetHeader>
1086
+ <SheetTitle>{{ siteData.name || 'Site' }}</SheetTitle>
1087
+ <SheetDescription />
1088
+ </SheetHeader>
1089
+ <edge-editor
1090
+ collection="sites"
1091
+ :doc-id="props.site"
1092
+ :schema="schemas.sites"
1093
+ :new-doc-schema="state.newDocs.sites"
1094
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none px-0 mx-0 shadow-none"
1095
+ :show-footer="false"
1096
+ :show-header="false"
1097
+ :save-function-override="onSubmit"
1098
+ card-content-class="px-0"
1099
+ @error="formErrors"
1100
+ >
1101
+ <template #main="slotProps">
1102
+ <div class="p-6 space-y-4 h-[calc(100vh-140px)] overflow-y-auto">
1103
+ <edge-shad-input
1104
+ v-model="slotProps.workingDoc.name"
1105
+ name="name"
1106
+ label="Name"
1107
+ placeholder="Enter name"
1108
+ class="w-full"
1109
+ />
1110
+ <edge-shad-tags
1111
+ v-model="slotProps.workingDoc.domains"
1112
+ name="domains"
1113
+ label="Domains"
1114
+ placeholder="Add or remove domains"
1115
+ class="w-full"
1116
+ />
1117
+ <edge-shad-input
1118
+ v-model="slotProps.workingDoc.contactEmail"
1119
+ name="contactEmail"
1120
+ label="Contact Email"
1121
+ placeholder="name@example.com"
1122
+ class="w-full"
1123
+ />
1124
+ <edge-shad-select-tags
1125
+ v-if="isAdmin"
1126
+ :model-value="Array.isArray(slotProps.workingDoc.allowedThemes) ? slotProps.workingDoc.allowedThemes : []"
1127
+ name="allowedThemes"
1128
+ label="Allowed Themes"
1129
+ placeholder="Select allowed themes"
1130
+ class="w-full"
1131
+ :items="themeOptions"
1132
+ item-title="label"
1133
+ item-value="value"
1134
+ @update:model-value="(value) => {
1135
+ const normalized = Array.isArray(value) ? value : []
1136
+ slotProps.workingDoc.allowedThemes = normalized
1137
+ if (normalized.length && !normalized.includes(slotProps.workingDoc.theme)) {
1138
+ slotProps.workingDoc.theme = normalized[0] || ''
1139
+ }
1140
+ }"
1141
+ />
1142
+ <edge-shad-select
1143
+ :model-value="slotProps.workingDoc.theme || ''"
1144
+ name="theme"
1145
+ label="Theme"
1146
+ placeholder="Select a theme"
1147
+ class="w-full"
1148
+ :items="themeItemsForAllowed(slotProps.workingDoc.allowedThemes, slotProps.workingDoc.theme)"
1149
+ item-title="label"
1150
+ item-value="value"
1151
+ @update:model-value="value => (slotProps.workingDoc.theme = value || '')"
1152
+ />
1153
+ <div class="space-y-2">
1154
+ <label class="text-sm font-medium text-foreground flex items-center justify-between">
1155
+ Logo
1156
+ <edge-shad-button
1157
+ type="button"
1158
+ variant="link"
1159
+ class="px-0 h-auto text-sm"
1160
+ @click="state.logoPickerOpen = !state.logoPickerOpen"
1161
+ >
1162
+ {{ state.logoPickerOpen ? 'Hide picker' : 'Select logo' }}
1163
+ </edge-shad-button>
1164
+ </label>
1165
+ <div class="flex items-center gap-4">
1166
+ <div v-if="slotProps.workingDoc.logo" class="flex items-center gap-3">
1167
+ <img :src="slotProps.workingDoc.logo" alt="Logo preview" class="h-16 w-auto rounded-md border border-border bg-muted object-contain">
1168
+ <edge-shad-button
1169
+ type="button"
1170
+ variant="ghost"
1171
+ class="h-8"
1172
+ @click="slotProps.workingDoc.logo = ''"
1173
+ >
1174
+ Remove
1175
+ </edge-shad-button>
1176
+ </div>
1177
+ <span v-else class="text-sm text-muted-foreground italic">No logo selected</span>
1178
+ </div>
1179
+ <div v-if="state.logoPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
1180
+ <edge-cms-media-manager
1181
+ :site="props.site"
1182
+ :select-mode="true"
1183
+ :default-tags="['Logos']"
1184
+ @select="(url) => {
1185
+ slotProps.workingDoc.logo = url
1186
+ state.logoPickerOpen = false
1187
+ }"
1188
+ />
1189
+ </div>
1190
+ </div>
1191
+ <div class="space-y-2">
1192
+ <label class="text-sm font-medium text-foreground flex items-center justify-between">
1193
+ Favicon
1194
+ <edge-shad-button
1195
+ type="button"
1196
+ variant="link"
1197
+ class="px-0 h-auto text-sm"
1198
+ @click="state.faviconPickerOpen = !state.faviconPickerOpen"
1199
+ >
1200
+ {{ state.faviconPickerOpen ? 'Hide picker' : 'Select favicon' }}
1201
+ </edge-shad-button>
1202
+ </label>
1203
+ <div class="flex items-center gap-4">
1204
+ <div v-if="slotProps.workingDoc.favicon" class="flex items-center gap-3">
1205
+ <img :src="slotProps.workingDoc.favicon" alt="Favicon preview" class="h-12 w-12 rounded-md border border-border bg-muted object-contain">
1206
+ <edge-shad-button
1207
+ type="button"
1208
+ variant="ghost"
1209
+ class="h-8"
1210
+ @click="slotProps.workingDoc.favicon = ''"
1211
+ >
1212
+ Remove
1213
+ </edge-shad-button>
1214
+ </div>
1215
+ <span v-else class="text-sm text-muted-foreground italic">No favicon selected</span>
1216
+ </div>
1217
+ <div v-if="state.faviconPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
1218
+ <edge-cms-media-manager
1219
+ :site="props.site"
1220
+ :select-mode="true"
1221
+ :default-tags="['Logos']"
1222
+ @select="(url) => {
1223
+ slotProps.workingDoc.favicon = url
1224
+ state.faviconPickerOpen = false
1225
+ }"
1226
+ />
1227
+ </div>
1228
+ </div>
1229
+ <edge-shad-select
1230
+ :model-value="slotProps.workingDoc.menuPosition || ''"
1231
+ name="menuPosition"
1232
+ label="Menu Position"
1233
+ placeholder="Select menu position"
1234
+ class="w-full"
1235
+ :items="menuPositionOptions"
1236
+ item-title="label"
1237
+ item-value="value"
1238
+ @update:model-value="value => (slotProps.workingDoc.menuPosition = value || '')"
1239
+ />
1240
+ <edge-shad-select-tags
1241
+ v-if="Object.keys(orgUsers).length > 0 && isAdmin"
1242
+ v-model="slotProps.workingDoc.users" :disabled="!edgeGlobal.isAdminGlobal(edgeFirebase).value"
1243
+ :items="Object.values(orgUsers)" name="users" label="Users"
1244
+ item-title="meta.name" item-value="userId" placeholder="Select users" class="w-full" :multiple="true"
1245
+ />
1246
+ <Card>
1247
+ <CardHeader>
1248
+ <CardTitle>SEO</CardTitle>
1249
+ <CardDescription>Default settings if the information is not entered on the page.</CardDescription>
1250
+ </CardHeader>
1251
+ <CardContent class="pt-0">
1252
+ <edge-shad-input
1253
+ v-model="slotProps.workingDoc.metaTitle"
1254
+ label="Meta Title"
1255
+ name="metaTitle"
1256
+ />
1257
+ <edge-shad-textarea
1258
+ v-model="slotProps.workingDoc.metaDescription"
1259
+ label="Meta Description"
1260
+ name="metaDescription"
1261
+ />
1262
+ <edge-cms-code-editor
1263
+ v-model="slotProps.workingDoc.structuredData"
1264
+ title="Structured Data (JSON-LD)"
1265
+ language="json"
1266
+ name="structuredData"
1267
+ height="300px"
1268
+ class="mb-4 w-full"
1269
+ />
1270
+ </CardContent>
1271
+ </Card>
1272
+ </div>
1273
+ <SheetFooter class="pt-2 flex justify-between">
1274
+ <edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">
1275
+ Cancel
1276
+ </edge-shad-button>
1277
+ <edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1278
+ <Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
1279
+ Update
1280
+ </edge-shad-button>
1281
+ </SheetFooter>
1282
+ </template>
1283
+ </edge-editor>
1284
+ </SheetContent>
1285
+ </Sheet>
1286
+ </div>
1287
+ </template>
1288
+
1289
+ <style scoped>
1290
+ .fade-enter-active,
1291
+ .fade-leave-active {
1292
+ transition: opacity 0.2s ease;
1293
+ }
1294
+ .fade-enter-from,
1295
+ .fade-leave-to {
1296
+ opacity: 0;
1297
+ }
1298
+ </style>