@edgedev/create-edge-app 1.1.25 → 1.1.27

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