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