@edgedev/create-edge-app 1.1.27 → 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,31 +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': '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
- },
48
+ sites: createSiteSettingsNewDocSchema(),
62
49
  },
63
50
  mounted: false,
64
51
  page: {},
@@ -67,20 +54,21 @@ const state = reactive({
67
54
  siteSettings: false,
68
55
  hasError: false,
69
56
  updating: false,
70
- logoPickerOpen: false,
71
- logoLightPickerOpen: false,
72
- brandLogoDarkPickerOpen: false,
73
- brandLogoLightPickerOpen: false,
74
- faviconPickerOpen: false,
75
57
  aiSectionOpen: false,
76
58
  selectedPostId: '',
77
59
  viewMode: 'pages',
60
+ submissionFilter: '',
61
+ selectedSubmissionId: '',
62
+ publishSiteLoading: false,
78
63
  })
79
64
 
80
65
  const pageInit = {
81
66
  name: '',
82
67
  content: [],
83
68
  blockIds: [],
69
+ metaTitle: '',
70
+ metaDescription: '',
71
+ structuredData: buildPageStructuredData(),
84
72
  }
85
73
 
86
74
  const schemas = {
@@ -95,6 +83,7 @@ const schemas = {
95
83
  path: ['domains', 0],
96
84
  }),
97
85
  contactEmail: z.string().optional(),
86
+ contactPhone: z.string().optional(),
98
87
  theme: z.string({
99
88
  required_error: 'Theme is required',
100
89
  }).min(1, { message: 'Theme is required' }),
@@ -110,6 +99,15 @@ const schemas = {
110
99
  metaTitle: z.string().optional(),
111
100
  metaDescription: z.string().optional(),
112
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(),
113
111
  aiAgentUserId: z.string().optional(),
114
112
  aiInstructions: z.string().optional(),
115
113
  })),
@@ -127,6 +125,185 @@ const isAdmin = computed(() => {
127
125
  const siteData = computed(() => {
128
126
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site] || {}
129
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
+ })
130
307
 
131
308
  const themeCollection = computed(() => {
132
309
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {}
@@ -197,6 +374,8 @@ const menuPositionOptions = [
197
374
  { value: 'right', label: 'Right' },
198
375
  ]
199
376
 
377
+ const isExternalLinkEntry = entry => entry?.item && typeof entry.item === 'object' && entry.item.type === 'external'
378
+
200
379
  const TEMPLATE_PAGES_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
201
380
  const seededSiteIds = new Set()
202
381
 
@@ -279,6 +458,7 @@ const deriveBlockIdsFromDoc = (doc = {}) => {
279
458
 
280
459
  const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') => {
281
460
  const timestamp = Date.now()
461
+ const templateStructuredData = typeof templateDoc?.structuredData === 'string' ? templateDoc.structuredData.trim() : ''
282
462
  const payload = {
283
463
  name: displayName?.trim()?.length ? displayName : titleFromSlug(slug),
284
464
  slug,
@@ -290,7 +470,7 @@ const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') =>
290
470
  blockIds: [],
291
471
  metaTitle: templateDoc?.metaTitle || '',
292
472
  metaDescription: templateDoc?.metaDescription || '',
293
- structuredData: templateDoc?.structuredData || '',
473
+ structuredData: templateStructuredData || buildPageStructuredData(),
294
474
  doc_created_at: timestamp,
295
475
  last_updated: timestamp,
296
476
  }
@@ -310,6 +490,8 @@ const buildMenusFromDefaultPages = (defaultPages = []) => {
310
490
  menus['Site Root'].push({
311
491
  name: slug,
312
492
  item: entry.pageId,
493
+ disableRename: !!entry?.disableRename,
494
+ disableDelete: !!entry?.disableDelete,
313
495
  })
314
496
  }
315
497
  return menus
@@ -323,6 +505,37 @@ const deriveThemeMenus = (themeDoc = {}) => {
323
505
  return null
324
506
  }
325
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
+
326
539
  const ensureTemplatePagesSnapshot = async () => {
327
540
  if (!edgeFirebase.data?.[TEMPLATE_PAGES_PATH.value])
328
541
  await edgeFirebase.startSnapshot(TEMPLATE_PAGES_PATH.value)
@@ -339,6 +552,10 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
339
552
  for (const entry of entries) {
340
553
  if (!entry || entry.item == null)
341
554
  continue
555
+ if (isExternalLinkEntry(entry)) {
556
+ next.push(edgeGlobal.dupObject(entry))
557
+ continue
558
+ }
342
559
  if (typeof entry.item === 'string' || entry.item === '') {
343
560
  const templateDoc = templatePages?.[entry.item] || null
344
561
  const slug = ensureUniqueSlug(entry.name || '', templateDoc, usedSlugs)
@@ -376,21 +593,26 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
376
593
  return next
377
594
  }
378
595
 
379
- const seedNewSiteFromTheme = async (siteId, themeId) => {
596
+ const seedNewSiteFromTheme = async (siteId, themeId, siteDoc) => {
380
597
  if (!siteId || !themeId)
381
598
  return
382
599
  const themeDoc = themeCollection.value?.[themeId]
383
600
  if (!themeDoc)
384
601
  return
602
+ const updatePayload = {}
385
603
  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 })
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)
394
616
  }
395
617
 
396
618
  const handleNewSiteSaved = async ({ docId, data, collection }) => {
@@ -405,7 +627,7 @@ const handleNewSiteSaved = async ({ docId, data, collection }) => {
405
627
  return
406
628
  seededSiteIds.add(docId)
407
629
  try {
408
- await seedNewSiteFromTheme(docId, themeId)
630
+ await seedNewSiteFromTheme(docId, themeId, data)
409
631
  }
410
632
  catch (error) {
411
633
  console.error('Failed to seed site from theme defaults', error)
@@ -438,6 +660,12 @@ onBeforeMount(async () => {
438
660
  if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`]) {
439
661
  await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`)
440
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
+ }
441
669
  state.mounted = true
442
670
  })
443
671
 
@@ -464,9 +692,19 @@ const isSiteDiff = computed(() => {
464
692
  favicon: publishedSite.favicon,
465
693
  menuPosition: publishedSite.menuPosition,
466
694
  contactEmail: publishedSite.contactEmail,
695
+ contactPhone: publishedSite.contactPhone,
467
696
  metaTitle: publishedSite.metaTitle,
468
697
  metaDescription: publishedSite.metaDescription,
469
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,
470
708
  }, {
471
709
  domains: siteData.value.domains,
472
710
  menus: siteData.value.menus,
@@ -481,9 +719,19 @@ const isSiteDiff = computed(() => {
481
719
  favicon: siteData.value.favicon,
482
720
  menuPosition: siteData.value.menuPosition,
483
721
  contactEmail: siteData.value.contactEmail,
722
+ contactPhone: siteData.value.contactPhone,
484
723
  metaTitle: siteData.value.metaTitle,
485
724
  metaDescription: siteData.value.metaDescription,
486
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,
487
735
  })
488
736
  }
489
737
  return false
@@ -512,9 +760,19 @@ const discardSiteSettings = async () => {
512
760
  favicon: publishedSite.favicon || '',
513
761
  menuPosition: publishedSite.menuPosition || '',
514
762
  contactEmail: publishedSite.contactEmail || '',
763
+ contactPhone: publishedSite.contactPhone || '',
515
764
  metaTitle: publishedSite.metaTitle || '',
516
765
  metaDescription: publishedSite.metaDescription || '',
517
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 || '',
518
776
  })
519
777
  }
520
778
  }
@@ -534,6 +792,19 @@ const publishSite = async () => {
534
792
  }
535
793
  }
536
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
+
537
808
  const pages = computed(() => {
538
809
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
539
810
  })
@@ -613,6 +884,8 @@ const setViewMode = (mode) => {
613
884
  return
614
885
  state.viewMode = mode
615
886
  state.selectedPostId = ''
887
+ if (mode !== 'submissions')
888
+ state.selectedSubmissionId = ''
616
889
  if (props.page)
617
890
  router.replace(pageRouteBase.value)
618
891
  }
@@ -662,19 +935,6 @@ watch(pages, (pagesCollection) => {
662
935
  state.menus = nextMenu
663
936
  }, { immediate: true, deep: true })
664
937
 
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
938
  watch(() => props.page, (next) => {
679
939
  if (next) {
680
940
  state.selectedPostId = ''
@@ -686,6 +946,20 @@ watch(() => props.page, (next) => {
686
946
  }
687
947
  })
688
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
+
689
963
  watch(() => state.menus, async (newVal) => {
690
964
  if (areEqualNormalized(siteData.value.menus, newVal)) {
691
965
  return
@@ -701,6 +975,8 @@ watch(() => state.menus, async (newVal) => {
701
975
  const newPage = JSON.parse(JSON.stringify(pageInit))
702
976
  for (const [menuName, items] of Object.entries(newVal)) {
703
977
  for (const [index, item] of items.entries()) {
978
+ if (isExternalLinkEntry(item))
979
+ continue
704
980
  if (typeof item.item === 'string') {
705
981
  if (item.item === '') {
706
982
  newPage.name = item.name
@@ -716,9 +992,11 @@ watch(() => state.menus, async (newVal) => {
716
992
  }
717
993
  }
718
994
  }
719
- if (typeof item.item === 'object') {
995
+ if (typeof item.item === 'object' && !isExternalLinkEntry(item)) {
720
996
  for (const [subMenuName, subItems] of Object.entries(item.item)) {
721
997
  for (const [subIndex, subItem] of subItems.entries()) {
998
+ if (isExternalLinkEntry(subItem))
999
+ continue
722
1000
  if (typeof subItem.item === 'string') {
723
1001
  if (subItem.item === '') {
724
1002
  newPage.name = subItem.name
@@ -988,18 +1266,45 @@ const pageSettingsUpdated = async (pageData) => {
988
1266
  <FilePenLine class="h-4 w-4" />
989
1267
  Posts
990
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>
991
1285
  </div>
992
1286
  </div>
993
1287
  <div v-if="!isTemplateSite" class="flex items-center gap-3 justify-end">
994
1288
  <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>
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>
1000
1305
  </div>
1001
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">
1002
- <FileCheck class="!text-green-800 w-3 h-3" />
1307
+ <FileCheck class="!text-green-800 w-3 h-6" />
1003
1308
  <span class="font-medium text-[10px]">
1004
1309
  Settings Published
1005
1310
  </span>
@@ -1050,7 +1355,147 @@ const pageSettingsUpdated = async (pageData) => {
1050
1355
  </div>
1051
1356
  <div class="flex-1">
1052
1357
  <Transition name="fade" mode="out-in">
1053
- <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">
1054
1499
  <edge-cms-posts
1055
1500
  mode="editor"
1056
1501
  :site="props.site"
@@ -1089,7 +1534,7 @@ const pageSettingsUpdated = async (pageData) => {
1089
1534
  </ResizablePanel>
1090
1535
  <ResizablePanel ref="mainPanel">
1091
1536
  <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">
1537
+ <div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-100px)] overflow-y-auto w-full">
1093
1538
  <NuxtPage class="flex flex-col flex-1 px-0 mx-0 pt-0" />
1094
1539
  </div>
1095
1540
  <div v-else class="p-4 text-center flex text-slate-500 h-[calc(100vh-4rem)] justify-center items-center overflow-y-auto">
@@ -1134,318 +1579,18 @@ const pageSettingsUpdated = async (pageData) => {
1134
1579
  >
1135
1580
  <template #main="slotProps">
1136
1581
  <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>
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"
1593
+ />
1449
1594
  </div>
1450
1595
  <SheetFooter class="pt-2 flex justify-between">
1451
1596
  <edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">