@edgedev/create-edge-app 1.1.26 → 1.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 {}
@@ -1001,6 +1091,21 @@ const unpublishedChangeDetails = computed(() => {
1001
1091
  return changes
1002
1092
  })
1003
1093
 
1094
+ const publishPage = async (pageId) => {
1095
+ if (state.publishLoading)
1096
+ return
1097
+ const pageData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
1098
+ if (!pageData[pageId])
1099
+ return
1100
+ state.publishLoading = true
1101
+ try {
1102
+ await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageData[pageId])
1103
+ }
1104
+ finally {
1105
+ state.publishLoading = false
1106
+ }
1107
+ }
1108
+
1004
1109
  const hasUnsavedChanges = (changes) => {
1005
1110
  console.log('Unsaved changes:', changes)
1006
1111
  if (changes === true) {
@@ -1034,14 +1139,25 @@ const hasUnsavedChanges = (changes) => {
1034
1139
  <div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
1035
1140
  <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
1141
  <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>
1142
+ <div class="flex items-center gap-2">
1143
+ <edge-shad-button
1144
+ variant="outline"
1145
+ class="bg-yellow-100 text-yellow-800 border-yellow-300 hover:bg-yellow-100 hover:text-yellow-900 text-xs h-[32px] gap-1"
1146
+ @click="state.showUnpublishedChangesDialog = true"
1147
+ >
1148
+ <AlertTriangle class="w-4 h-4" />
1149
+ Unpublished Changes
1150
+ </edge-shad-button>
1151
+ <edge-shad-button
1152
+ class="bg-primary text-primary-foreground hover:bg-primary/90 text-xs h-[32px] gap-1 shadow-sm"
1153
+ :disabled="state.publishLoading"
1154
+ @click="publishPage(page)"
1155
+ >
1156
+ <Loader2 v-if="state.publishLoading" class="w-4 h-4 animate-spin" />
1157
+ <UploadCloud v-else class="w-4 h-4" />
1158
+ Publish
1159
+ </edge-shad-button>
1160
+ </div>
1045
1161
  </template>
1046
1162
  <template v-else>
1047
1163
  <edge-chip class="bg-green-100 text-green-800">
@@ -1121,6 +1237,23 @@ const hasUnsavedChanges = (changes) => {
1121
1237
  </div>
1122
1238
  </div>
1123
1239
  </template>
1240
+ <template #success-alert>
1241
+ <div v-if="!props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
1242
+ <edge-shad-button
1243
+ variant="outline"
1244
+ class="text-xs h-[28px] gap-1"
1245
+ :disabled="state.seoAiLoading"
1246
+ @click="updateSeoWithAi"
1247
+ >
1248
+ <Loader2 v-if="state.seoAiLoading" class="w-3.5 h-3.5 animate-spin" />
1249
+ <Sparkles v-else class="w-3.5 h-3.5" />
1250
+ Update SEO with AI
1251
+ </edge-shad-button>
1252
+ <span v-if="state.seoAiError" class="text-xs text-destructive">
1253
+ {{ state.seoAiError }}
1254
+ </span>
1255
+ </div>
1256
+ </template>
1124
1257
  <template #main="slotProps">
1125
1258
  <Tabs class="w-full" default-value="list">
1126
1259
  <TabsList v-if="slotProps.workingDoc?.post" class="w-full mt-3 bg-primary rounded-sm">