@edgedev/create-edge-app 1.1.26 → 1.1.28

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.
@@ -1,8 +1,9 @@
1
1
  <script setup lang="js">
2
2
  import { toTypedSchema } from '@vee-validate/zod'
3
3
  import * as z from 'zod'
4
+ import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Inbox, Loader2, Mail, MailOpen, MoreHorizontal } from 'lucide-vue-next'
5
+ import { useStructuredDataTemplates } from '@/edge/composables/structuredDataTemplates'
4
6
 
5
- import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Loader2, MoreHorizontal } from 'lucide-vue-next'
6
7
  const props = defineProps({
7
8
  site: {
8
9
  type: String,
@@ -15,6 +16,8 @@ const props = defineProps({
15
16
  },
16
17
  })
17
18
  const edgeFirebase = inject('edgeFirebase')
19
+ const { createDefaults: createSiteSettingsDefaults, createNewDocSchema: createSiteSettingsNewDocSchema } = useSiteSettingsTemplate()
20
+ const { buildPageStructuredData } = useStructuredDataTemplates()
18
21
 
19
22
  const normalizeForCompare = (value) => {
20
23
  if (Array.isArray(value))
@@ -34,26 +37,15 @@ const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
34
37
  const isTemplateSite = computed(() => props.site === 'templates')
35
38
  const router = useRouter()
36
39
 
40
+ const SUBMISSION_IGNORE_FIELDS = new Set(['orgId', 'siteId', 'pageId', 'blockId'])
41
+ const SUBMISSION_LABEL_KEYS = ['name', 'fullName', 'firstName', 'lastName', 'email', 'phone']
42
+ const SUBMISSION_MESSAGE_KEYS = ['message', 'comments', 'notes', 'inquiry', 'details']
43
+
37
44
  const state = reactive({
38
45
  filter: '',
39
46
  userFilter: 'all',
40
47
  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
- },
48
+ sites: createSiteSettingsNewDocSchema(),
57
49
  },
58
50
  mounted: false,
59
51
  page: {},
@@ -62,17 +54,21 @@ const state = reactive({
62
54
  siteSettings: false,
63
55
  hasError: false,
64
56
  updating: false,
65
- logoPickerOpen: false,
66
- faviconPickerOpen: false,
67
57
  aiSectionOpen: false,
68
58
  selectedPostId: '',
69
59
  viewMode: 'pages',
60
+ submissionFilter: '',
61
+ selectedSubmissionId: '',
62
+ publishSiteLoading: false,
70
63
  })
71
64
 
72
65
  const pageInit = {
73
66
  name: '',
74
67
  content: [],
75
68
  blockIds: [],
69
+ metaTitle: '',
70
+ metaDescription: '',
71
+ structuredData: buildPageStructuredData(),
76
72
  }
77
73
 
78
74
  const schemas = {
@@ -87,16 +83,31 @@ const schemas = {
87
83
  path: ['domains', 0],
88
84
  }),
89
85
  contactEmail: z.string().optional(),
86
+ contactPhone: z.string().optional(),
90
87
  theme: z.string({
91
88
  required_error: 'Theme is required',
92
89
  }).min(1, { message: 'Theme is required' }),
93
90
  allowedThemes: z.array(z.string()).optional(),
94
91
  logo: z.string().optional(),
92
+ logoLight: z.string().optional(),
93
+ logoText: z.string().optional(),
94
+ logoType: z.enum(['image', 'text']).optional(),
95
+ brandLogoDark: z.string().optional(),
96
+ brandLogoLight: z.string().optional(),
95
97
  favicon: z.string().optional(),
96
98
  menuPosition: z.enum(['left', 'center', 'right']).optional(),
97
99
  metaTitle: z.string().optional(),
98
100
  metaDescription: z.string().optional(),
99
101
  structuredData: z.string().optional(),
102
+ trackingFacebookPixel: z.string().optional(),
103
+ trackingGoogleAnalytics: z.string().optional(),
104
+ trackingAdroll: z.string().optional(),
105
+ socialFacebook: z.string().optional(),
106
+ socialInstagram: z.string().optional(),
107
+ socialTwitter: z.string().optional(),
108
+ socialLinkedIn: z.string().optional(),
109
+ socialYouTube: z.string().optional(),
110
+ socialTikTok: z.string().optional(),
100
111
  aiAgentUserId: z.string().optional(),
101
112
  aiInstructions: z.string().optional(),
102
113
  })),
@@ -114,6 +125,185 @@ const isAdmin = computed(() => {
114
125
  const siteData = computed(() => {
115
126
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site] || {}
116
127
  })
128
+ const publishedSiteSettings = computed(() => {
129
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`]?.[props.site] || {}
130
+ })
131
+ const domainError = computed(() => {
132
+ return String(publishedSiteSettings.value?.domainError || '').trim()
133
+ })
134
+
135
+ const submissionsCollection = computed(() => `sites/${props.site}/lead-actions`)
136
+ const isViewingSubmissions = computed(() => state.viewMode === 'submissions')
137
+ const submissionsMap = computed(() => {
138
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`] || {}
139
+ })
140
+ const selectedSubmission = computed(() => {
141
+ return submissionsMap.value?.[state.selectedSubmissionId] || null
142
+ })
143
+ const unreadSubmissionsCount = computed(() => {
144
+ return Object.values(submissionsMap.value || {}).filter((item) => {
145
+ if (item?.action !== 'Contact Form')
146
+ return false
147
+ return !item.readAt
148
+ }).length
149
+ })
150
+
151
+ const formatSubmissionValue = (value) => {
152
+ if (value === undefined || value === null)
153
+ return ''
154
+ if (typeof value === 'string')
155
+ return value
156
+ if (typeof value === 'number' || typeof value === 'boolean')
157
+ return String(value)
158
+ try {
159
+ return JSON.stringify(value)
160
+ }
161
+ catch {
162
+ return String(value)
163
+ }
164
+ }
165
+
166
+ const collectSubmissionEntries = (data) => {
167
+ if (!data || typeof data !== 'object')
168
+ return []
169
+ const entries = []
170
+ const seen = new Set()
171
+ const addEntry = (key, value) => {
172
+ const normalizedKey = String(key || '').trim()
173
+ if (!normalizedKey)
174
+ return
175
+ const lowerKey = normalizedKey.toLowerCase()
176
+ if (SUBMISSION_IGNORE_FIELDS.has(normalizedKey) || SUBMISSION_IGNORE_FIELDS.has(lowerKey))
177
+ return
178
+ if (value === undefined || value === null || value === '')
179
+ return
180
+ if (seen.has(lowerKey))
181
+ return
182
+ entries.push({ key: normalizedKey, value })
183
+ seen.add(lowerKey)
184
+ }
185
+
186
+ const addArrayFields = (fields) => {
187
+ if (!Array.isArray(fields))
188
+ return
189
+ fields.forEach((field) => {
190
+ if (!field)
191
+ return
192
+ const name = field.field || field.name || field.fieldName || field.label || field.title
193
+ const value = field.value ?? field.fieldValue ?? field.val
194
+ addEntry(name, value)
195
+ })
196
+ }
197
+
198
+ addArrayFields(data.fields)
199
+ addArrayFields(data.formFields)
200
+ addArrayFields(data.formData)
201
+
202
+ if (data.fields && typeof data.fields === 'object' && !Array.isArray(data.fields)) {
203
+ Object.entries(data.fields).forEach(([key, value]) => addEntry(key, value))
204
+ }
205
+
206
+ Object.entries(data).forEach(([key, value]) => {
207
+ if (key === 'fields' || key === 'formFields' || key === 'formData')
208
+ return
209
+ addEntry(key, value)
210
+ })
211
+
212
+ return entries.sort((a, b) => String(a.key).localeCompare(String(b.key)))
213
+ }
214
+
215
+ const getSubmissionLabel = (data) => {
216
+ if (!data || typeof data !== 'object')
217
+ return 'Contact Form Submission'
218
+ const name = [data.firstName, data.lastName].filter(Boolean).join(' ').trim()
219
+ if (name)
220
+ return name
221
+ const direct = SUBMISSION_LABEL_KEYS.find(key => String(data[key] || '').trim().length)
222
+ if (direct)
223
+ return String(data[direct]).trim()
224
+ return 'Contact Form Submission'
225
+ }
226
+
227
+ const getSubmissionMessage = (data) => {
228
+ if (!data || typeof data !== 'object')
229
+ return ''
230
+ const direct = SUBMISSION_MESSAGE_KEYS.find(key => String(data[key] || '').trim().length)
231
+ if (direct)
232
+ return String(data[direct]).trim()
233
+ return ''
234
+ }
235
+
236
+ const formatSubmissionKey = (key) => {
237
+ return String(key || '')
238
+ .trim()
239
+ .replace(/_/g, ' ')
240
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
241
+ .replace(/\s+/g, ' ')
242
+ .replace(/^./, str => str.toUpperCase())
243
+ }
244
+
245
+ const getSubmissionEntriesPreview = (data, limit = 6) => {
246
+ return collectSubmissionEntries(data).slice(0, limit)
247
+ }
248
+
249
+ const formatSubmissionTimestamp = (timestamp) => {
250
+ const date = timestamp?.toDate?.() || (timestamp ? new Date(timestamp) : null)
251
+ if (!date || Number.isNaN(date.getTime()))
252
+ return ''
253
+ return new Intl.DateTimeFormat('en-US', {
254
+ dateStyle: 'medium',
255
+ timeStyle: 'short',
256
+ }).format(date)
257
+ }
258
+
259
+ const isSubmissionUnread = item => item && item.action === 'Contact Form' && !item.readAt
260
+
261
+ const markSubmissionRead = async (docId) => {
262
+ const item = submissionsMap.value?.[docId]
263
+ if (!item || !isSubmissionUnread(item))
264
+ return
265
+ try {
266
+ await edgeFirebase.changeDoc(
267
+ `${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`,
268
+ docId,
269
+ { readAt: new Date().toISOString() },
270
+ )
271
+ }
272
+ catch (error) {
273
+ console.error('Failed to mark submission as read', error)
274
+ }
275
+ }
276
+
277
+ const markSubmissionUnread = async (docId) => {
278
+ const item = submissionsMap.value?.[docId]
279
+ if (!item || isSubmissionUnread(item))
280
+ return
281
+ try {
282
+ await edgeFirebase.changeDoc(
283
+ `${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`,
284
+ docId,
285
+ { readAt: null },
286
+ )
287
+ }
288
+ catch (error) {
289
+ console.error('Failed to mark submission as unread', error)
290
+ }
291
+ }
292
+
293
+ const getSubmissionSortTime = (item) => {
294
+ const date = item?.timestamp?.toDate?.() || (item?.timestamp ? new Date(item.timestamp) : null)
295
+ if (!date || Number.isNaN(date.getTime()))
296
+ return 0
297
+ return date.getTime()
298
+ }
299
+
300
+ const sortedSubmissionIds = computed(() => {
301
+ return Object.values(submissionsMap.value || {})
302
+ .filter(item => item?.docId)
303
+ .map(item => ({ id: item.docId, time: getSubmissionSortTime(item) }))
304
+ .sort((a, b) => b.time - a.time)
305
+ .map(item => item.id)
306
+ })
117
307
 
118
308
  const themeCollection = computed(() => {
119
309
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {}
@@ -184,6 +374,8 @@ const menuPositionOptions = [
184
374
  { value: 'right', label: 'Right' },
185
375
  ]
186
376
 
377
+ const isExternalLinkEntry = entry => entry?.item && typeof entry.item === 'object' && entry.item.type === 'external'
378
+
187
379
  const TEMPLATE_PAGES_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
188
380
  const seededSiteIds = new Set()
189
381
 
@@ -266,6 +458,7 @@ const deriveBlockIdsFromDoc = (doc = {}) => {
266
458
 
267
459
  const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') => {
268
460
  const timestamp = Date.now()
461
+ const templateStructuredData = typeof templateDoc?.structuredData === 'string' ? templateDoc.structuredData.trim() : ''
269
462
  const payload = {
270
463
  name: displayName?.trim()?.length ? displayName : titleFromSlug(slug),
271
464
  slug,
@@ -277,7 +470,7 @@ const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') =>
277
470
  blockIds: [],
278
471
  metaTitle: templateDoc?.metaTitle || '',
279
472
  metaDescription: templateDoc?.metaDescription || '',
280
- structuredData: templateDoc?.structuredData || '',
473
+ structuredData: templateStructuredData || buildPageStructuredData(),
281
474
  doc_created_at: timestamp,
282
475
  last_updated: timestamp,
283
476
  }
@@ -297,6 +490,8 @@ const buildMenusFromDefaultPages = (defaultPages = []) => {
297
490
  menus['Site Root'].push({
298
491
  name: slug,
299
492
  item: entry.pageId,
493
+ disableRename: !!entry?.disableRename,
494
+ disableDelete: !!entry?.disableDelete,
300
495
  })
301
496
  }
302
497
  return menus
@@ -310,6 +505,37 @@ const deriveThemeMenus = (themeDoc = {}) => {
310
505
  return null
311
506
  }
312
507
 
508
+ const shouldApplyThemeSetting = (currentValue, baseValue) => {
509
+ if (currentValue === undefined || currentValue === null)
510
+ return true
511
+ if (typeof currentValue === 'string')
512
+ return !currentValue.trim() || areEqualNormalized(currentValue, baseValue)
513
+ if (Array.isArray(currentValue))
514
+ return currentValue.length === 0 || areEqualNormalized(currentValue, baseValue)
515
+ if (typeof currentValue === 'object')
516
+ return Object.keys(currentValue).length === 0 || areEqualNormalized(currentValue, baseValue)
517
+ return areEqualNormalized(currentValue, baseValue)
518
+ }
519
+
520
+ const buildThemeSettingsPayload = (themeDoc = {}, siteDoc = {}) => {
521
+ if (!themeDoc?.defaultSiteSettings || typeof themeDoc.defaultSiteSettings !== 'object' || Array.isArray(themeDoc.defaultSiteSettings))
522
+ return {}
523
+ const baseDefaults = createSiteSettingsDefaults()
524
+ const payload = {}
525
+ for (const [key, baseValue] of Object.entries(baseDefaults)) {
526
+ if (!(key in themeDoc.defaultSiteSettings))
527
+ continue
528
+ let themeValue = themeDoc.defaultSiteSettings[key]
529
+ if (key === 'structuredData' && typeof themeValue === 'string' && !themeValue.trim())
530
+ themeValue = baseValue
531
+ if (areEqualNormalized(themeValue, baseValue))
532
+ continue
533
+ if (shouldApplyThemeSetting(siteDoc?.[key], baseValue))
534
+ payload[key] = themeValue
535
+ }
536
+ return payload
537
+ }
538
+
313
539
  const ensureTemplatePagesSnapshot = async () => {
314
540
  if (!edgeFirebase.data?.[TEMPLATE_PAGES_PATH.value])
315
541
  await edgeFirebase.startSnapshot(TEMPLATE_PAGES_PATH.value)
@@ -326,6 +552,10 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
326
552
  for (const entry of entries) {
327
553
  if (!entry || entry.item == null)
328
554
  continue
555
+ if (isExternalLinkEntry(entry)) {
556
+ next.push(edgeGlobal.dupObject(entry))
557
+ continue
558
+ }
329
559
  if (typeof entry.item === 'string' || entry.item === '') {
330
560
  const templateDoc = templatePages?.[entry.item] || null
331
561
  const slug = ensureUniqueSlug(entry.name || '', templateDoc, usedSlugs)
@@ -363,21 +593,26 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
363
593
  return next
364
594
  }
365
595
 
366
- const seedNewSiteFromTheme = async (siteId, themeId) => {
596
+ const seedNewSiteFromTheme = async (siteId, themeId, siteDoc) => {
367
597
  if (!siteId || !themeId)
368
598
  return
369
599
  const themeDoc = themeCollection.value?.[themeId]
370
600
  if (!themeDoc)
371
601
  return
602
+ const updatePayload = {}
372
603
  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 })
604
+ if (themeMenus) {
605
+ const templatePages = await ensureTemplatePagesSnapshot()
606
+ const usedSlugs = new Set()
607
+ const seededMenus = ensureMenuBuckets(themeMenus)
608
+ seededMenus['Site Root'] = await duplicateEntriesWithPages(seededMenus['Site Root'], { templatePages, siteId, usedSlugs })
609
+ seededMenus['Not In Menu'] = await duplicateEntriesWithPages(seededMenus['Not In Menu'], { templatePages, siteId, usedSlugs })
610
+ updatePayload.menus = seededMenus
611
+ }
612
+ const settingsPayload = buildThemeSettingsPayload(themeDoc, siteDoc || {})
613
+ Object.assign(updatePayload, settingsPayload)
614
+ if (Object.keys(updatePayload).length)
615
+ await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites`, siteId, updatePayload)
381
616
  }
382
617
 
383
618
  const handleNewSiteSaved = async ({ docId, data, collection }) => {
@@ -392,7 +627,7 @@ const handleNewSiteSaved = async ({ docId, data, collection }) => {
392
627
  return
393
628
  seededSiteIds.add(docId)
394
629
  try {
395
- await seedNewSiteFromTheme(docId, themeId)
630
+ await seedNewSiteFromTheme(docId, themeId, data)
396
631
  }
397
632
  catch (error) {
398
633
  console.error('Failed to seed site from theme defaults', error)
@@ -425,6 +660,12 @@ onBeforeMount(async () => {
425
660
  if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`]) {
426
661
  await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`)
427
662
  }
663
+ if (props.site !== 'templates') {
664
+ const submissionsPath = `organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/lead-actions`
665
+ if (!edgeFirebase.data?.[submissionsPath]) {
666
+ await edgeFirebase.startSnapshot(submissionsPath, [{ field: 'action', operator: '==', value: 'Contact Form' }])
667
+ }
668
+ }
428
669
  state.mounted = true
429
670
  })
430
671
 
@@ -443,24 +684,54 @@ const isSiteDiff = computed(() => {
443
684
  theme: publishedSite.theme,
444
685
  allowedThemes: publishedSite.allowedThemes,
445
686
  logo: publishedSite.logo,
687
+ logoLight: publishedSite.logoLight,
688
+ logoText: publishedSite.logoText,
689
+ logoType: publishedSite.logoType,
690
+ brandLogoDark: publishedSite.brandLogoDark,
691
+ brandLogoLight: publishedSite.brandLogoLight,
446
692
  favicon: publishedSite.favicon,
447
693
  menuPosition: publishedSite.menuPosition,
448
694
  contactEmail: publishedSite.contactEmail,
695
+ contactPhone: publishedSite.contactPhone,
449
696
  metaTitle: publishedSite.metaTitle,
450
697
  metaDescription: publishedSite.metaDescription,
451
698
  structuredData: publishedSite.structuredData,
699
+ trackingFacebookPixel: publishedSite.trackingFacebookPixel,
700
+ trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics,
701
+ trackingAdroll: publishedSite.trackingAdroll,
702
+ socialFacebook: publishedSite.socialFacebook,
703
+ socialInstagram: publishedSite.socialInstagram,
704
+ socialTwitter: publishedSite.socialTwitter,
705
+ socialLinkedIn: publishedSite.socialLinkedIn,
706
+ socialYouTube: publishedSite.socialYouTube,
707
+ socialTikTok: publishedSite.socialTikTok,
452
708
  }, {
453
709
  domains: siteData.value.domains,
454
710
  menus: siteData.value.menus,
455
711
  theme: siteData.value.theme,
456
712
  allowedThemes: siteData.value.allowedThemes,
457
713
  logo: siteData.value.logo,
714
+ logoLight: siteData.value.logoLight,
715
+ logoText: siteData.value.logoText,
716
+ logoType: siteData.value.logoType,
717
+ brandLogoDark: siteData.value.brandLogoDark,
718
+ brandLogoLight: siteData.value.brandLogoLight,
458
719
  favicon: siteData.value.favicon,
459
720
  menuPosition: siteData.value.menuPosition,
460
721
  contactEmail: siteData.value.contactEmail,
722
+ contactPhone: siteData.value.contactPhone,
461
723
  metaTitle: siteData.value.metaTitle,
462
724
  metaDescription: siteData.value.metaDescription,
463
725
  structuredData: siteData.value.structuredData,
726
+ trackingFacebookPixel: siteData.value.trackingFacebookPixel,
727
+ trackingGoogleAnalytics: siteData.value.trackingGoogleAnalytics,
728
+ trackingAdroll: siteData.value.trackingAdroll,
729
+ socialFacebook: siteData.value.socialFacebook,
730
+ socialInstagram: siteData.value.socialInstagram,
731
+ socialTwitter: siteData.value.socialTwitter,
732
+ socialLinkedIn: siteData.value.socialLinkedIn,
733
+ socialYouTube: siteData.value.socialYouTube,
734
+ socialTikTok: siteData.value.socialTikTok,
464
735
  })
465
736
  }
466
737
  return false
@@ -481,12 +752,27 @@ const discardSiteSettings = async () => {
481
752
  theme: publishedSite.theme || '',
482
753
  allowedThemes: publishedSite.allowedThemes || [],
483
754
  logo: publishedSite.logo || '',
755
+ logoLight: publishedSite.logoLight || '',
756
+ logoText: publishedSite.logoText || '',
757
+ logoType: publishedSite.logoType || 'image',
758
+ brandLogoDark: publishedSite.brandLogoDark || '',
759
+ brandLogoLight: publishedSite.brandLogoLight || '',
484
760
  favicon: publishedSite.favicon || '',
485
761
  menuPosition: publishedSite.menuPosition || '',
486
762
  contactEmail: publishedSite.contactEmail || '',
763
+ contactPhone: publishedSite.contactPhone || '',
487
764
  metaTitle: publishedSite.metaTitle || '',
488
765
  metaDescription: publishedSite.metaDescription || '',
489
766
  structuredData: publishedSite.structuredData || '',
767
+ trackingFacebookPixel: publishedSite.trackingFacebookPixel || '',
768
+ trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics || '',
769
+ trackingAdroll: publishedSite.trackingAdroll || '',
770
+ socialFacebook: publishedSite.socialFacebook || '',
771
+ socialInstagram: publishedSite.socialInstagram || '',
772
+ socialTwitter: publishedSite.socialTwitter || '',
773
+ socialLinkedIn: publishedSite.socialLinkedIn || '',
774
+ socialYouTube: publishedSite.socialYouTube || '',
775
+ socialTikTok: publishedSite.socialTikTok || '',
490
776
  })
491
777
  }
492
778
  }
@@ -506,6 +792,19 @@ const publishSite = async () => {
506
792
  }
507
793
  }
508
794
 
795
+ const publishSiteAndSettings = async () => {
796
+ if (state.publishSiteLoading)
797
+ return
798
+ state.publishSiteLoading = true
799
+ try {
800
+ await publishSiteSettings()
801
+ await publishSite()
802
+ }
803
+ finally {
804
+ state.publishSiteLoading = false
805
+ }
806
+ }
807
+
509
808
  const pages = computed(() => {
510
809
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
511
810
  })
@@ -585,6 +884,8 @@ const setViewMode = (mode) => {
585
884
  return
586
885
  state.viewMode = mode
587
886
  state.selectedPostId = ''
887
+ if (mode !== 'submissions')
888
+ state.selectedSubmissionId = ''
588
889
  if (props.page)
589
890
  router.replace(pageRouteBase.value)
590
891
  }
@@ -634,13 +935,6 @@ watch(pages, (pagesCollection) => {
634
935
  state.menus = nextMenu
635
936
  }, { immediate: true, deep: true })
636
937
 
637
- watch(() => state.siteSettings, (open) => {
638
- if (!open)
639
- state.logoPickerOpen = false
640
- if (!open)
641
- state.faviconPickerOpen = false
642
- })
643
-
644
938
  watch(() => props.page, (next) => {
645
939
  if (next) {
646
940
  state.selectedPostId = ''
@@ -652,6 +946,20 @@ watch(() => props.page, (next) => {
652
946
  }
653
947
  })
654
948
 
949
+ watch([isViewingSubmissions, sortedSubmissionIds], () => {
950
+ if (!isViewingSubmissions.value)
951
+ return
952
+ const ids = sortedSubmissionIds.value
953
+ if (!ids.length) {
954
+ state.selectedSubmissionId = ''
955
+ return
956
+ }
957
+ if (!state.selectedSubmissionId || !submissionsMap.value?.[state.selectedSubmissionId]) {
958
+ state.selectedSubmissionId = ids[0]
959
+ markSubmissionRead(ids[0])
960
+ }
961
+ }, { immediate: true })
962
+
655
963
  watch(() => state.menus, async (newVal) => {
656
964
  if (areEqualNormalized(siteData.value.menus, newVal)) {
657
965
  return
@@ -667,6 +975,8 @@ watch(() => state.menus, async (newVal) => {
667
975
  const newPage = JSON.parse(JSON.stringify(pageInit))
668
976
  for (const [menuName, items] of Object.entries(newVal)) {
669
977
  for (const [index, item] of items.entries()) {
978
+ if (isExternalLinkEntry(item))
979
+ continue
670
980
  if (typeof item.item === 'string') {
671
981
  if (item.item === '') {
672
982
  newPage.name = item.name
@@ -682,9 +992,11 @@ watch(() => state.menus, async (newVal) => {
682
992
  }
683
993
  }
684
994
  }
685
- if (typeof item.item === 'object') {
995
+ if (typeof item.item === 'object' && !isExternalLinkEntry(item)) {
686
996
  for (const [subMenuName, subItems] of Object.entries(item.item)) {
687
997
  for (const [subIndex, subItem] of subItems.entries()) {
998
+ if (isExternalLinkEntry(subItem))
999
+ continue
688
1000
  if (typeof subItem.item === 'string') {
689
1001
  if (subItem.item === '') {
690
1002
  newPage.name = subItem.name
@@ -954,18 +1266,45 @@ const pageSettingsUpdated = async (pageData) => {
954
1266
  <FilePenLine class="h-4 w-4" />
955
1267
  Posts
956
1268
  </edge-shad-button>
1269
+ <edge-shad-button
1270
+ variant="ghost"
1271
+ size="sm"
1272
+ class="h-8 px-4 text-xs gap-2 rounded-full"
1273
+ :class="state.viewMode === 'submissions' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
1274
+ @click="setViewMode('submissions')"
1275
+ >
1276
+ <Inbox class="h-4 w-4" />
1277
+ Inbox
1278
+ <span
1279
+ v-if="unreadSubmissionsCount"
1280
+ class="ml-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground"
1281
+ >
1282
+ {{ unreadSubmissionsCount }}
1283
+ </span>
1284
+ </edge-shad-button>
957
1285
  </div>
958
1286
  </div>
959
1287
  <div v-if="!isTemplateSite" class="flex items-center gap-3 justify-end">
960
1288
  <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>
1289
+ <div v-if="isSiteDiff || isAnyPagesDiff" key="unpublished" class="flex gap-2 items-center">
1290
+ <div class="flex gap-1 items-center bg-yellow-100 text-xs py-1 px-3 text-yellow-800 rounded">
1291
+ <CircleAlert class="!text-yellow-800 w-3 h-6" />
1292
+ <span class="font-medium text-[10px]">
1293
+ {{ isSiteDiff ? 'Unpublished Settings' : 'Unpublished Pages' }}
1294
+ </span>
1295
+ </div>
1296
+ <edge-shad-button
1297
+ class="h-8 px-4 text-xs gap-2 bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm"
1298
+ :disabled="state.publishSiteLoading"
1299
+ @click="publishSiteAndSettings"
1300
+ >
1301
+ <Loader2 v-if="state.publishSiteLoading" class="h-3.5 w-3.5 animate-spin" />
1302
+ <FolderUp v-else class="h-3.5 w-3.5" />
1303
+ Publish Site
1304
+ </edge-shad-button>
966
1305
  </div>
967
1306
  <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" />
1307
+ <FileCheck class="!text-green-800 w-3 h-6" />
969
1308
  <span class="font-medium text-[10px]">
970
1309
  Settings Published
971
1310
  </span>
@@ -1016,7 +1355,147 @@ const pageSettingsUpdated = async (pageData) => {
1016
1355
  </div>
1017
1356
  <div class="flex-1">
1018
1357
  <Transition name="fade" mode="out-in">
1019
- <div v-if="isEditingPost" class="w-full h-full">
1358
+ <div v-if="isViewingSubmissions" class="flex-1 overflow-y-auto p-6">
1359
+ <edge-dashboard
1360
+ :collection="submissionsCollection"
1361
+ query-field="action"
1362
+ query-value="Contact Form"
1363
+ query-operator="=="
1364
+ :filter="state.submissionFilter"
1365
+ :filter-fields="['data.name', 'data.fullName', 'data.firstName', 'data.lastName', 'data.email', 'data.phone', 'data.message', 'data.comments', 'data.notes']"
1366
+ sort-field="timestamp"
1367
+ sort-direction="desc"
1368
+ class="pt-0 flex-1"
1369
+ >
1370
+ <template #header-start>
1371
+ <Inbox class="mr-2 h-4 w-4" />
1372
+ Submissions
1373
+ <!-- <span class="ml-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1374
+ Contact Form
1375
+ </span> -->
1376
+ </template>
1377
+ <template #header-center>
1378
+ <div class="w-full px-4 md:px-6">
1379
+ <edge-shad-input
1380
+ v-model="state.submissionFilter"
1381
+ name="submissionFilter"
1382
+ placeholder="Search submissions..."
1383
+ class="w-full"
1384
+ />
1385
+ </div>
1386
+ </template>
1387
+ <template #header-end="slotProps">
1388
+ <span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1389
+ {{ slotProps.recordCount }} total • {{ unreadSubmissionsCount }} unread
1390
+ </span>
1391
+ </template>
1392
+ <template #list="slotProps">
1393
+ <div class="grid gap-4 pt-4 w-full md:grid-cols-[320px_minmax(0,1fr)]">
1394
+ <div class="space-y-2">
1395
+ <div
1396
+ v-for="item in slotProps.filtered"
1397
+ :key="item.docId"
1398
+ role="button"
1399
+ tabindex="0"
1400
+ class="group rounded-lg border p-3 text-left transition hover:border-primary/60 hover:bg-muted/60"
1401
+ :class="state.selectedSubmissionId === item.docId ? 'border-primary/70 bg-muted/70 shadow-sm' : 'border-border/60 bg-card'"
1402
+ @click="state.selectedSubmissionId = item.docId; markSubmissionRead(item.docId)"
1403
+ @keyup.enter="state.selectedSubmissionId = item.docId; markSubmissionRead(item.docId)"
1404
+ >
1405
+ <div class="flex items-start justify-between gap-2">
1406
+ <div class="min-w-0">
1407
+ <div class="truncate text-sm font-semibold text-foreground">
1408
+ {{ getSubmissionLabel(item.data) }}
1409
+ </div>
1410
+ <div v-if="item.data?.pageName" class="truncate text-xs text-muted-foreground">
1411
+ {{ item.data.pageName }}
1412
+ </div>
1413
+ </div>
1414
+ <div class="flex items-center gap-2 text-[11px] text-muted-foreground">
1415
+ <span v-if="isSubmissionUnread(item)" class="rounded-full bg-primary/15 px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">
1416
+ Unread
1417
+ </span>
1418
+ <span>{{ formatSubmissionTimestamp(item.timestamp) }}</span>
1419
+ </div>
1420
+ </div>
1421
+ <div v-if="getSubmissionMessage(item.data)" class="mt-2 text-xs text-muted-foreground line-clamp-2">
1422
+ {{ getSubmissionMessage(item.data) }}
1423
+ </div>
1424
+ </div>
1425
+ </div>
1426
+ <div>
1427
+ <Card v-if="selectedSubmission" class="border border-border/70 bg-card/95 shadow-sm">
1428
+ <CardHeader class="flex flex-col gap-2">
1429
+ <div class="flex flex-wrap items-start justify-between gap-2">
1430
+ <div>
1431
+ <CardTitle class="text-xl">
1432
+ {{ getSubmissionLabel(selectedSubmission.data) }}
1433
+ </CardTitle>
1434
+ <CardDescription class="text-xs">
1435
+ {{ formatSubmissionTimestamp(selectedSubmission.timestamp) }}
1436
+ </CardDescription>
1437
+ </div>
1438
+ <div class="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1439
+ <span>
1440
+ {{ selectedSubmission.data?.pageName || selectedSubmission.data?.pageId || 'Site submission' }}
1441
+ </span>
1442
+ <edge-shad-button
1443
+ v-if="isSubmissionUnread(selectedSubmission)"
1444
+ size="sm"
1445
+ variant="outline"
1446
+ class="h-7 gap-2 text-[11px]"
1447
+ @click="markSubmissionRead(selectedSubmission.docId)"
1448
+ >
1449
+ <MailOpen class="h-3.5 w-3.5" />
1450
+ Mark read
1451
+ </edge-shad-button>
1452
+ <edge-shad-button
1453
+ v-else
1454
+ size="sm"
1455
+ variant="outline"
1456
+ class="h-7 gap-2 text-[11px]"
1457
+ @click="markSubmissionUnread(selectedSubmission.docId)"
1458
+ >
1459
+ <Mail class="h-3.5 w-3.5" />
1460
+ Mark unread
1461
+ </edge-shad-button>
1462
+ </div>
1463
+ </div>
1464
+ </CardHeader>
1465
+ <CardContent class="space-y-4">
1466
+ <div
1467
+ v-if="getSubmissionMessage(selectedSubmission.data)"
1468
+ class="rounded-lg border border-border/60 bg-muted/40 p-3 text-sm text-foreground"
1469
+ >
1470
+ {{ getSubmissionMessage(selectedSubmission.data) }}
1471
+ </div>
1472
+ <div class="grid gap-3 md:grid-cols-2">
1473
+ <div
1474
+ v-for="entry in collectSubmissionEntries(selectedSubmission.data)"
1475
+ :key="entry.key"
1476
+ class="rounded-lg border border-border/60 bg-background p-3"
1477
+ >
1478
+ <div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1479
+ {{ formatSubmissionKey(entry.key) }}
1480
+ </div>
1481
+ <div class="mt-1 text-sm text-foreground break-words">
1482
+ {{ formatSubmissionValue(entry.value) }}
1483
+ </div>
1484
+ </div>
1485
+ </div>
1486
+ </CardContent>
1487
+ </Card>
1488
+ <Card v-else class="border border-dashed border-border/80 bg-muted/30">
1489
+ <CardContent class="py-12 text-center text-sm text-muted-foreground">
1490
+ Select a submission to view details.
1491
+ </CardContent>
1492
+ </Card>
1493
+ </div>
1494
+ </div>
1495
+ </template>
1496
+ </edge-dashboard>
1497
+ </div>
1498
+ <div v-else-if="isEditingPost" class="w-full h-full">
1020
1499
  <edge-cms-posts
1021
1500
  mode="editor"
1022
1501
  :site="props.site"
@@ -1055,7 +1534,7 @@ const pageSettingsUpdated = async (pageData) => {
1055
1534
  </ResizablePanel>
1056
1535
  <ResizablePanel ref="mainPanel">
1057
1536
  <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">
1537
+ <div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-100px)] overflow-y-auto w-full">
1059
1538
  <NuxtPage class="flex flex-col flex-1 px-0 mx-0 pt-0" />
1060
1539
  </div>
1061
1540
  <div v-else class="p-4 text-center flex text-slate-500 h-[calc(100vh-4rem)] justify-center items-center overflow-y-auto">
@@ -1099,176 +1578,19 @@ const pageSettingsUpdated = async (pageData) => {
1099
1578
  @error="formErrors"
1100
1579
  >
1101
1580
  <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"
1581
+ <div class="p-6 h-[calc(100vh-140px)] overflow-y-auto">
1582
+ <edge-cms-site-settings-form
1583
+ :settings="slotProps.workingDoc"
1584
+ :theme-options="themeOptions"
1585
+ :user-options="userOptions"
1586
+ :has-users="Object.keys(orgUsers).length > 0"
1587
+ :show-users="true"
1588
+ :show-theme-fields="true"
1589
+ :is-admin="isAdmin"
1590
+ :enable-media-picker="true"
1591
+ :site-id="props.site"
1592
+ :domain-error="domainError"
1109
1593
  />
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
1594
  </div>
1273
1595
  <SheetFooter class="pt-2 flex justify-between">
1274
1596
  <edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">