@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.
- package/edge/components/cms/block.vue +334 -26
- package/edge/components/cms/blockEditor.vue +50 -3
- package/edge/components/cms/codeEditor.vue +15 -0
- package/edge/components/cms/init_blocks/footer.html +111 -19
- package/edge/components/cms/init_blocks/image.html +8 -0
- package/edge/components/cms/init_blocks/post_content.html +3 -2
- package/edge/components/cms/init_blocks/post_title_header.html +8 -6
- package/edge/components/cms/init_blocks/posts_list.html +6 -5
- package/edge/components/cms/mediaCard.vue +13 -2
- package/edge/components/cms/mediaManager.vue +16 -2
- package/edge/components/cms/menu.vue +253 -42
- package/edge/components/cms/page.vue +151 -18
- package/edge/components/cms/site.vue +517 -372
- package/edge/components/cms/siteSettingsForm.vue +616 -0
- package/edge/components/cms/themeDefaultMenu.vue +258 -22
- package/edge/components/cms/themeEditor.vue +95 -11
- package/edge/components/editor.vue +1 -0
- package/edge/components/formSubtypes/myOrgs.vue +112 -1
- package/edge/components/orgSwitcher.vue +1 -1
- package/edge/components/organizationMembers.vue +171 -21
- package/edge/components/shad/html.vue +6 -0
- package/edge/components/sideBar.vue +7 -4
- package/edge/components/sideBarContent.vue +1 -1
- package/edge/components/userMenu.vue +50 -14
- package/edge/composables/siteSettingsTemplate.js +79 -0
- package/edge/composables/structuredDataTemplates.js +36 -0
- package/package.json +1 -1
|
@@ -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:
|
|
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 (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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-
|
|
996
|
-
<
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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-
|
|
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="
|
|
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-
|
|
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
|
-
<
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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">
|