@edgedev/create-edge-app 1.1.27 → 1.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/edge/components/auth/register.vue +51 -0
  2. package/edge/components/cms/block.vue +363 -42
  3. package/edge/components/cms/blockEditor.vue +50 -3
  4. package/edge/components/cms/codeEditor.vue +39 -2
  5. package/edge/components/cms/htmlContent.vue +10 -2
  6. package/edge/components/cms/init_blocks/footer.html +111 -19
  7. package/edge/components/cms/init_blocks/image.html +8 -0
  8. package/edge/components/cms/init_blocks/post_content.html +3 -2
  9. package/edge/components/cms/init_blocks/post_title_header.html +8 -6
  10. package/edge/components/cms/init_blocks/posts_list.html +6 -5
  11. package/edge/components/cms/mediaCard.vue +13 -2
  12. package/edge/components/cms/mediaManager.vue +35 -5
  13. package/edge/components/cms/menu.vue +384 -61
  14. package/edge/components/cms/optionsSelect.vue +20 -3
  15. package/edge/components/cms/page.vue +160 -18
  16. package/edge/components/cms/site.vue +548 -374
  17. package/edge/components/cms/siteSettingsForm.vue +623 -0
  18. package/edge/components/cms/themeDefaultMenu.vue +258 -22
  19. package/edge/components/cms/themeEditor.vue +95 -11
  20. package/edge/components/editor.vue +1 -0
  21. package/edge/components/formSubtypes/myOrgs.vue +112 -1
  22. package/edge/components/imagePicker.vue +126 -0
  23. package/edge/components/myAccount.vue +1 -0
  24. package/edge/components/myProfile.vue +345 -61
  25. package/edge/components/orgSwitcher.vue +1 -1
  26. package/edge/components/organizationMembers.vue +620 -235
  27. package/edge/components/shad/html.vue +6 -0
  28. package/edge/components/shad/number.vue +2 -2
  29. package/edge/components/sideBar.vue +7 -4
  30. package/edge/components/sideBarContent.vue +1 -1
  31. package/edge/components/userMenu.vue +50 -14
  32. package/edge/composables/global.ts +4 -1
  33. package/edge/composables/siteSettingsTemplate.js +79 -0
  34. package/edge/composables/structuredDataTemplates.js +36 -0
  35. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  import { useVModel } from '@vueuse/core'
3
3
  const props = defineProps({
4
4
  modelValue: {
5
- type: [String, Boolean, Number, null],
5
+ type: [String, Boolean, Number, Array, null],
6
6
  required: false,
7
7
  default: null,
8
8
  },
@@ -14,6 +14,10 @@ const props = defineProps({
14
14
  type: String,
15
15
  required: false,
16
16
  },
17
+ multiple: {
18
+ type: Boolean,
19
+ default: false,
20
+ },
17
21
  })
18
22
  const emits = defineEmits(['update:modelValue'])
19
23
  const edgeFirebase = inject('edgeFirebase')
@@ -91,17 +95,30 @@ onBeforeMount(async () => {
91
95
  })
92
96
  .filter(Boolean) // remove nulls
93
97
  }
94
- staticOption.options.unshift({ title: '(none)', name: NONE_VALUE })
98
+ if (!props.multiple) {
99
+ staticOption.options.unshift({ title: '(none)', name: NONE_VALUE })
100
+ }
95
101
  state.loading = false
96
102
  })
97
103
  </script>
98
104
 
99
105
  <template>
100
106
  <edge-shad-select
101
- v-if="!state.loading && staticOption.options.length > 0"
107
+ v-if="!state.loading && staticOption.options.length > 0 && !props.multiple"
102
108
  v-model="selectValue"
103
109
  :label="props.label"
104
110
  :name="props.option.field"
105
111
  :items="staticOption.options"
106
112
  />
113
+ <edge-shad-select-tags
114
+ v-else-if="!state.loading && staticOption.options.length > 0 && props.multiple"
115
+ :model-value="Array.isArray(modelValue) ? modelValue : []"
116
+ :label="props.label"
117
+ :name="props.option.field"
118
+ :items="staticOption.options"
119
+ item-title="title"
120
+ item-value="name"
121
+ :allow-additions="false"
122
+ @update:model-value="value => (modelValue = Array.isArray(value) ? value : [])"
123
+ />
107
124
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { AlertTriangle, ArrowDown, ArrowUp, Maximize2, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
2
+ import { AlertTriangle, ArrowDown, ArrowUp, Maximize2, Monitor, Smartphone, Sparkles, Tablet, UploadCloud } from 'lucide-vue-next'
3
3
  import { toTypedSchema } from '@vee-validate/zod'
4
4
  import * as z from 'zod'
5
5
  const props = defineProps({
@@ -20,6 +20,7 @@ const props = defineProps({
20
20
  const emit = defineEmits(['head'])
21
21
 
22
22
  const edgeFirebase = inject('edgeFirebase')
23
+ const { buildPageStructuredData } = useStructuredDataTemplates()
23
24
 
24
25
  const state = reactive({
25
26
  newDocs: {
@@ -29,11 +30,17 @@ const state = reactive({
29
30
  postContent: { value: [] },
30
31
  structure: { value: [] },
31
32
  postStructure: { value: [] },
33
+ metaTitle: { value: '' },
34
+ metaDescription: { value: '' },
35
+ structuredData: { value: buildPageStructuredData() },
32
36
  },
33
37
  },
34
38
  editMode: false,
35
39
  showUnpublishedChangesDialog: false,
40
+ publishLoading: false,
36
41
  workingDoc: {},
42
+ seoAiLoading: false,
43
+ seoAiError: '',
37
44
  previewViewport: 'full',
38
45
  newRowLayout: '6',
39
46
  newPostRowLayout: '6',
@@ -200,6 +207,40 @@ const ensureBlocksArray = (workingDoc, key) => {
200
207
  }
201
208
  }
202
209
 
210
+ const applySeoAiResults = (payload) => {
211
+ if (!payload || typeof payload !== 'object')
212
+ return
213
+ if (payload.metaTitle)
214
+ state.workingDoc.metaTitle = payload.metaTitle
215
+ if (payload.metaDescription)
216
+ state.workingDoc.metaDescription = payload.metaDescription
217
+ if (payload.structuredData)
218
+ state.workingDoc.structuredData = payload.structuredData
219
+ }
220
+
221
+ const updateSeoWithAi = async () => {
222
+ if (!edgeFirebase?.user?.uid)
223
+ return
224
+ state.seoAiLoading = true
225
+ state.seoAiError = ''
226
+ try {
227
+ const results = await edgeFirebase.runFunction('cms-updateSeoFromAi', {
228
+ orgId: edgeGlobal.edgeState.currentOrganization,
229
+ siteId: props.site,
230
+ pageId: props.page,
231
+ uid: edgeFirebase.user.uid,
232
+ })
233
+ applySeoAiResults(results?.data || {})
234
+ }
235
+ catch (error) {
236
+ console.error('Failed to update SEO with AI', error)
237
+ state.seoAiError = 'Failed to update SEO. Try again.'
238
+ }
239
+ finally {
240
+ state.seoAiLoading = false
241
+ }
242
+ }
243
+
203
244
  const createRow = (columns = 1) => {
204
245
  const row = {
205
246
  id: edgeGlobal.generateShortId(),
@@ -398,6 +439,54 @@ const blockPick = (block, index, slotProps, post = false) => {
398
439
  }
399
440
  }
400
441
 
442
+ const applyCollectionUniqueKeys = (workingDoc) => {
443
+ const resolveUniqueKey = (template) => {
444
+ if (!template || typeof template !== 'string')
445
+ return ''
446
+ let resolved = template
447
+ const orgId = edgeGlobal.edgeState.currentOrganization || ''
448
+ const siteId = props.site || ''
449
+ if (resolved.includes('{orgId}') && orgId)
450
+ resolved = resolved.replaceAll('{orgId}', orgId)
451
+ if (resolved.includes('{siteId}') && siteId)
452
+ resolved = resolved.replaceAll('{siteId}', siteId)
453
+ return resolved
454
+ }
455
+
456
+ const applyToBlocks = (blocks) => {
457
+ if (!Array.isArray(blocks))
458
+ return
459
+ blocks.forEach((block) => {
460
+ const meta = block?.meta
461
+ if (!meta || typeof meta !== 'object')
462
+ return
463
+ Object.keys(meta).forEach((fieldKey) => {
464
+ const cfg = meta[fieldKey]
465
+ if (!cfg?.collection?.uniqueKey)
466
+ return
467
+ const resolved = resolveUniqueKey(cfg.collection.uniqueKey)
468
+ if (!resolved)
469
+ return
470
+ if (cfg.queryItems && !Object.prototype.hasOwnProperty.call(cfg, 'uniqueKey')) {
471
+ const reordered = {}
472
+ Object.keys(cfg).forEach((key) => {
473
+ reordered[key] = cfg[key]
474
+ if (key === 'queryItems')
475
+ reordered.uniqueKey = resolved
476
+ })
477
+ block.meta[fieldKey] = reordered
478
+ }
479
+ else {
480
+ cfg.uniqueKey = resolved
481
+ }
482
+ })
483
+ })
484
+ }
485
+
486
+ applyToBlocks(workingDoc?.content)
487
+ applyToBlocks(workingDoc?.postContent)
488
+ }
489
+
401
490
  onMounted(() => {
402
491
  if (props.page === 'new') {
403
492
  state.editMode = true
@@ -408,6 +497,7 @@ const editorDocUpdates = (workingDoc) => {
408
497
  ensureStructureDefaults(workingDoc, false)
409
498
  if (workingDoc?.post || (Array.isArray(workingDoc?.postContent) && workingDoc.postContent.length > 0) || Array.isArray(workingDoc?.postStructure))
410
499
  ensureStructureDefaults(workingDoc, true)
500
+ applyCollectionUniqueKeys(workingDoc)
411
501
  const blockIds = (workingDoc.content || []).map(block => block.blockId).filter(id => id)
412
502
  const postBlockIds = workingDoc.postContent ? workingDoc.postContent.map(block => block.blockId).filter(id => id) : []
413
503
  blockIds.push(...postBlockIds)
@@ -520,6 +610,15 @@ const layoutSpansFromString = (value, fallback = [6]) => {
520
610
 
521
611
  const rowUsesSpans = row => (row?.columns || []).some(col => Number.isFinite(col?.span))
522
612
 
613
+ const rowGapClass = (row) => {
614
+ const gap = Number(row?.gap)
615
+ const allowed = new Set([0, 2, 4, 6, 8])
616
+ const safeGap = allowed.has(gap) ? gap : 4
617
+ if (safeGap === 0)
618
+ return 'gap-0'
619
+ return ['gap-0', `sm:gap-${safeGap}`].join(' ')
620
+ }
621
+
523
622
  const rowGridClass = (row) => {
524
623
  const base = isMobilePreview.value
525
624
  ? 'grid grid-cols-1'
@@ -542,15 +641,6 @@ const rowVerticalAlignClass = (row) => {
542
641
  return map[row?.verticalAlign] || map.start
543
642
  }
544
643
 
545
- const rowGapClass = (row) => {
546
- const gap = Number(row?.gap)
547
- const allowed = new Set([0, 2, 4, 6, 8])
548
- const safeGap = allowed.has(gap) ? gap : 4
549
- if (safeGap === 0)
550
- return 'gap-0'
551
- return ['gap-0', `sm:gap-${safeGap}`].join(' ')
552
- }
553
-
554
644
  const rowGridStyle = (row) => {
555
645
  if (isMobilePreview.value)
556
646
  return {}
@@ -791,6 +881,9 @@ const isPublishedPageDiff = (pageId) => {
791
881
  metaTitle: publishedPage.metaTitle,
792
882
  metaDescription: publishedPage.metaDescription,
793
883
  structuredData: publishedPage.structuredData,
884
+ postMetaTitle: publishedPage.postMetaTitle,
885
+ postMetaDescription: publishedPage.postMetaDescription,
886
+ postStructuredData: publishedPage.postStructuredData,
794
887
  },
795
888
  {
796
889
  content: draftPage.content,
@@ -800,6 +893,9 @@ const isPublishedPageDiff = (pageId) => {
800
893
  metaTitle: draftPage.metaTitle,
801
894
  metaDescription: draftPage.metaDescription,
802
895
  structuredData: draftPage.structuredData,
896
+ postMetaTitle: draftPage.postMetaTitle,
897
+ postMetaDescription: draftPage.postMetaDescription,
898
+ postStructuredData: draftPage.postStructuredData,
803
899
  },
804
900
  )
805
901
  }
@@ -997,10 +1093,28 @@ const unpublishedChangeDetails = computed(() => {
997
1093
  compareField('metaTitle', 'Meta title', val => summarizeChangeValue(val, true))
998
1094
  compareField('metaDescription', 'Meta description', val => summarizeChangeValue(val, true))
999
1095
  compareField('structuredData', 'Structured data', val => summarizeChangeValue(val, true))
1096
+ compareField('postMetaTitle', 'Detail meta title', val => summarizeChangeValue(val, true))
1097
+ compareField('postMetaDescription', 'Detail meta description', val => summarizeChangeValue(val, true))
1098
+ compareField('postStructuredData', 'Detail structured data', val => summarizeChangeValue(val, true))
1000
1099
 
1001
1100
  return changes
1002
1101
  })
1003
1102
 
1103
+ const publishPage = async (pageId) => {
1104
+ if (state.publishLoading)
1105
+ return
1106
+ const pageData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
1107
+ if (!pageData[pageId])
1108
+ return
1109
+ state.publishLoading = true
1110
+ try {
1111
+ await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageData[pageId])
1112
+ }
1113
+ finally {
1114
+ state.publishLoading = false
1115
+ }
1116
+ }
1117
+
1004
1118
  const hasUnsavedChanges = (changes) => {
1005
1119
  console.log('Unsaved changes:', changes)
1006
1120
  if (changes === true) {
@@ -1034,14 +1148,25 @@ const hasUnsavedChanges = (changes) => {
1034
1148
  <div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
1035
1149
  <div v-if="!props.isTemplateSite" class="px-4 text-gray-600 dark:text-gray-300 whitespace-nowrap text-center flex flex-col items-center gap-1">
1036
1150
  <template v-if="isPublishedPageDiff(page)">
1037
- <edge-shad-button
1038
- variant="outline"
1039
- class="bg-yellow-100 text-yellow-800 border-yellow-300 hover:bg-yellow-100 hover:text-yellow-900 text-xs h-[32px] gap-1"
1040
- @click="state.showUnpublishedChangesDialog = true"
1041
- >
1042
- <AlertTriangle class="w-4 h-4" />
1043
- Unpublished Changes
1044
- </edge-shad-button>
1151
+ <div class="flex items-center gap-2">
1152
+ <edge-shad-button
1153
+ variant="outline"
1154
+ class="bg-yellow-100 text-yellow-800 border-yellow-300 hover:bg-yellow-100 hover:text-yellow-900 text-xs h-[32px] gap-1"
1155
+ @click="state.showUnpublishedChangesDialog = true"
1156
+ >
1157
+ <AlertTriangle class="w-4 h-4" />
1158
+ Unpublished Changes
1159
+ </edge-shad-button>
1160
+ <edge-shad-button
1161
+ class="bg-primary text-primary-foreground hover:bg-primary/90 text-xs h-[32px] gap-1 shadow-sm"
1162
+ :disabled="state.publishLoading"
1163
+ @click="publishPage(page)"
1164
+ >
1165
+ <Loader2 v-if="state.publishLoading" class="w-4 h-4 animate-spin" />
1166
+ <UploadCloud v-else class="w-4 h-4" />
1167
+ Publish
1168
+ </edge-shad-button>
1169
+ </div>
1045
1170
  </template>
1046
1171
  <template v-else>
1047
1172
  <edge-chip class="bg-green-100 text-green-800">
@@ -1121,6 +1246,23 @@ const hasUnsavedChanges = (changes) => {
1121
1246
  </div>
1122
1247
  </div>
1123
1248
  </template>
1249
+ <template #success-alert>
1250
+ <div v-if="!props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
1251
+ <edge-shad-button
1252
+ variant="outline"
1253
+ class="text-xs h-[28px] gap-1"
1254
+ :disabled="state.seoAiLoading"
1255
+ @click="updateSeoWithAi"
1256
+ >
1257
+ <Loader2 v-if="state.seoAiLoading" class="w-3.5 h-3.5 animate-spin" />
1258
+ <Sparkles v-else class="w-3.5 h-3.5" />
1259
+ Update SEO with AI
1260
+ </edge-shad-button>
1261
+ <span v-if="state.seoAiError" class="text-xs text-destructive">
1262
+ {{ state.seoAiError }}
1263
+ </span>
1264
+ </div>
1265
+ </template>
1124
1266
  <template #main="slotProps">
1125
1267
  <Tabs class="w-full" default-value="list">
1126
1268
  <TabsList v-if="slotProps.workingDoc?.post" class="w-full mt-3 bg-primary rounded-sm">