cinqcinqdev-seo 0.1.31 → 0.1.33
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/dist/module.json +1 -1
- package/dist/runtime/assets/admin-tw.css +1 -1
- package/dist/runtime/components/admin-cms/ContentCalendar.vue +7 -7
- package/dist/runtime/components/admin-cms/DynamicRenderer.vue +2 -2
- package/dist/runtime/components/admin-cms/Navbar.vue +5 -5
- package/dist/runtime/components/admin-cms/Sidebar.vue +8 -8
- package/dist/runtime/components/sections/ContactForm.vue +15 -11
- package/dist/runtime/components/sections/FAQ.vue +9 -5
- package/dist/runtime/components/sections/Features.vue +10 -6
- package/dist/runtime/components/sections/HeroSection.vue +19 -15
- package/dist/runtime/components/sections/Pricing.vue +12 -8
- package/dist/runtime/components/sections/Process.vue +13 -9
- package/dist/runtime/components/sections/ServiceGrid.vue +14 -10
- package/dist/runtime/components/sections/Testimonials.vue +16 -12
- package/dist/runtime/components/sections/TextVisual.vue +15 -11
- package/dist/runtime/pages/admin/account.vue +31 -31
- package/dist/runtime/pages/admin/branding.vue +14 -14
- package/dist/runtime/pages/admin/calendar.vue +1 -1
- package/dist/runtime/pages/admin/content-library.vue +16 -16
- package/dist/runtime/pages/admin/editor/[id].vue +114 -114
- package/dist/runtime/pages/admin/index.vue +20 -20
- package/dist/runtime/pages/admin/pages/[type].vue +54 -54
- package/dist/runtime/pages/admin/personas/[id].vue +9 -9
- package/dist/runtime/pages/admin/personas/index.vue +6 -6
- package/dist/runtime/pages/admin/products.vue +7 -7
- package/dist/runtime/pages/admin/seo.vue +36 -36
- package/dist/runtime/pages/admin/setup.vue +6 -6
- package/dist/runtime/pages/admin/strategies/[id].vue +7 -7
- package/dist/runtime/pages/admin/strategies/index.vue +7 -7
- package/dist/runtime/pages/admin/todos.vue +8 -8
- package/package.json +1 -1
|
@@ -405,14 +405,14 @@ const savePage = async () => {
|
|
|
405
405
|
<input type="file" ref="fileInput" class="hidden" accept="image/*" @change="handleFileUpload" />
|
|
406
406
|
|
|
407
407
|
<!-- ── Top Bar ── -->
|
|
408
|
-
<header class="h-12 bg-
|
|
408
|
+
<header class="h-12 bg-white/80 backdrop-blur-md border-b px-6 flex items-center justify-between z-[60] shrink-0">
|
|
409
409
|
<div class="flex items-center gap-4">
|
|
410
|
-
<NuxtLink :to="basePath" class="text-
|
|
411
|
-
<div class="flex gap-1 bg-
|
|
412
|
-
<button @click="isLeftSidebarOpen = !isLeftSidebarOpen" :class="['p-1.5 rounded transition-all', isLeftSidebarOpen ? 'bg-
|
|
410
|
+
<NuxtLink :to="basePath" class="text-gray-400 hover:text-black transition-colors">←</NuxtLink>
|
|
411
|
+
<div class="flex gap-1 bg-gray-100 p-1 rounded-md">
|
|
412
|
+
<button @click="isLeftSidebarOpen = !isLeftSidebarOpen" :class="['p-1.5 rounded transition-all', isLeftSidebarOpen ? 'bg-white shadow-sm text-[#3d35ff]' : 'text-gray-400']" title="Structure">
|
|
413
413
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M4 6h16M4 12h10M4 18h16"/></svg>
|
|
414
414
|
</button>
|
|
415
|
-
<button @click="isRightSidebarOpen = !isRightSidebarOpen" :class="['p-1.5 rounded transition-all', isRightSidebarOpen ? 'bg-
|
|
415
|
+
<button @click="isRightSidebarOpen = !isRightSidebarOpen" :class="['p-1.5 rounded transition-all', isRightSidebarOpen ? 'bg-white shadow-sm text-[#3d35ff]' : 'text-gray-400']" title="Propriétés">
|
|
416
416
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 6h8M12 12h8M12 18h8M4 6h.01M4 12h.01M4 18h.01"/></svg>
|
|
417
417
|
</button>
|
|
418
418
|
</div>
|
|
@@ -420,23 +420,23 @@ const savePage = async () => {
|
|
|
420
420
|
</div>
|
|
421
421
|
|
|
422
422
|
<div class="flex items-center gap-4">
|
|
423
|
-
<div class="flex bg-
|
|
424
|
-
<button @click="viewMode = 'structure'" :class="['px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest', viewMode === 'structure' ? 'bg-
|
|
425
|
-
<button @click="viewMode = 'seo'" :class="['px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest', viewMode === 'seo' ? 'bg-
|
|
423
|
+
<div class="flex bg-gray-100 p-1 rounded-full scale-90">
|
|
424
|
+
<button @click="viewMode = 'structure'" :class="['px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest', viewMode === 'structure' ? 'bg-white text-black' : 'text-gray-400']">Structure</button>
|
|
425
|
+
<button @click="viewMode = 'seo'" :class="['px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest', viewMode === 'seo' ? 'bg-white text-black' : 'text-gray-400']">Audit SEO</button>
|
|
426
426
|
</div>
|
|
427
|
-
<NuxtLink v-if="page?.slug" :to="pageViewUrl" target="_blank" class="flex items-center gap-1.5 px-4 py-2 rounded-full text-[9px] font-black uppercase tracking-widest border border-
|
|
427
|
+
<NuxtLink v-if="page?.slug" :to="pageViewUrl" target="_blank" class="flex items-center gap-1.5 px-4 py-2 rounded-full text-[9px] font-black uppercase tracking-widest border border-gray-200 text-gray-500 hover:border-[#3d35ff] hover:text-[#3d35ff] transition-all">
|
|
428
428
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
|
429
429
|
Voir la page
|
|
430
430
|
</NuxtLink>
|
|
431
431
|
<button @click="togglePublished"
|
|
432
432
|
:class="[
|
|
433
433
|
'flex items-center gap-1.5 px-4 py-2 rounded-full text-[9px] font-black uppercase tracking-widest border transition-all active:scale-95',
|
|
434
|
-
page?.status === 'published' ? 'bg-green-50 border-green-200 text-green-600 hover:bg-green-100' : 'bg-
|
|
434
|
+
page?.status === 'published' ? 'bg-green-50 border-green-200 text-green-600 hover:bg-green-100' : 'bg-gray-50 border-gray-200 text-gray-400 hover:border-gray-300'
|
|
435
435
|
]">
|
|
436
|
-
<span :class="['w-1.5 h-1.5 rounded-full', page?.status === 'published' ? 'bg-green-500' : 'bg-
|
|
436
|
+
<span :class="['w-1.5 h-1.5 rounded-full', page?.status === 'published' ? 'bg-green-500' : 'bg-gray-300']"></span>
|
|
437
437
|
{{ page?.status === "published" ? "Publi\xE9" : "Brouillon" }}
|
|
438
438
|
</button>
|
|
439
|
-
<button @click="savePage" class="relative bg-
|
|
439
|
+
<button @click="savePage" class="relative bg-[#3d35ff] text-white px-5 py-2 rounded-full text-[9px] font-black uppercase tracking-widest shadow-lg active:scale-95 transition-all">
|
|
440
440
|
{{ isSaving ? "Enregistrement..." : "Mettre \xE0 jour" }}
|
|
441
441
|
<span v-if="!isSaving && dirtyBlocks.size > 0"
|
|
442
442
|
class="absolute -top-1.5 -right-1.5 w-4 h-4 bg-amber-400 text-white rounded-full text-[7px] font-black flex items-center justify-center">
|
|
@@ -449,10 +449,10 @@ const savePage = async () => {
|
|
|
449
449
|
<div class="flex flex-1 overflow-hidden relative">
|
|
450
450
|
|
|
451
451
|
<!-- ── Left Sidebar: Structure / SEO Tree ── -->
|
|
452
|
-
<aside :class="['bg-
|
|
452
|
+
<aside :class="['bg-white border-r z-50 transition-all duration-300 overflow-hidden flex flex-col', isLeftSidebarOpen ? 'w-64' : 'w-0']">
|
|
453
453
|
<div class="p-6 w-64 h-full flex flex-col">
|
|
454
454
|
<div v-if="viewMode === 'structure'" class="flex-1 space-y-2 overflow-y-auto">
|
|
455
|
-
<h3 class="text-[9px] font-black text-
|
|
455
|
+
<h3 class="text-[9px] font-black text-gray-300 uppercase mb-4 italic tracking-widest">Arborescence</h3>
|
|
456
456
|
<div v-if="page?.content_locked" class="p-4 rounded-xl bg-amber-50 border border-amber-100 text-amber-600 text-[8px] font-black uppercase tracking-widest text-center">
|
|
457
457
|
Page verrouillée<br/><span class="font-normal normal-case opacity-60 text-[7px]">Seul le SEO est éditable</span>
|
|
458
458
|
</div>
|
|
@@ -460,7 +460,7 @@ const savePage = async () => {
|
|
|
460
460
|
<div v-for="(block, index) in page.content" :key="index" @click="selectedBlockIndex = index"
|
|
461
461
|
:class="[
|
|
462
462
|
'p-3 rounded-xl border-2 transition-all cursor-pointer group flex items-center justify-between gap-2',
|
|
463
|
-
selectedBlockIndex === index ? 'border-
|
|
463
|
+
selectedBlockIndex === index ? 'border-[#3d35ff] bg-blue-50/50 text-[#3d35ff]' : dirtyBlocks.has(index) ? 'border-amber-300 bg-amber-50 text-amber-700' : 'border-gray-50 bg-gray-50 text-gray-500'
|
|
464
464
|
]">
|
|
465
465
|
<input
|
|
466
466
|
v-if="editingLabelIndex === index"
|
|
@@ -485,14 +485,14 @@ const savePage = async () => {
|
|
|
485
485
|
</div>
|
|
486
486
|
</div>
|
|
487
487
|
</div>
|
|
488
|
-
<button @click="showAddModal = true" class="w-full py-3 border-2 border-dashed border-
|
|
488
|
+
<button @click="showAddModal = true" class="w-full py-3 border-2 border-dashed border-gray-100 rounded-xl text-[9px] font-black text-gray-400 hover:border-[#3d35ff] transition-all">+ SECTION</button>
|
|
489
489
|
</template>
|
|
490
490
|
</div>
|
|
491
491
|
|
|
492
492
|
<div v-else class="flex-1 overflow-y-auto">
|
|
493
|
-
<h3 class="text-[9px] font-black text-
|
|
493
|
+
<h3 class="text-[9px] font-black text-[#3d35ff] uppercase mb-6 italic tracking-widest">Hiérarchie Hn</h3>
|
|
494
494
|
<div v-for="(node, nIdx) in seoTree" :key="nIdx"
|
|
495
|
-
class="mb-2 p-2 rounded border-l-2 bg-
|
|
495
|
+
class="mb-2 p-2 rounded border-l-2 bg-gray-50/50 cursor-pointer hover:bg-blue-50/50"
|
|
496
496
|
:style="{ marginLeft: `${tagDepth(node.tag) * 14}px`, borderColor: tagColor(node.tag) }"
|
|
497
497
|
@click="selectedBlockIndex = node.blockIndex">
|
|
498
498
|
<span class="text-[7px] font-black uppercase block" :style="{ color: tagColor(node.tag) }">{{ node.tag }}</span>
|
|
@@ -504,26 +504,26 @@ const savePage = async () => {
|
|
|
504
504
|
|
|
505
505
|
<!-- ── Preview ── -->
|
|
506
506
|
<main class="flex-1 overflow-y-auto p-4 md:p-10 bg-[#F5F5F7]">
|
|
507
|
-
<div class="max-w-4xl mx-auto bg-
|
|
507
|
+
<div class="max-w-4xl mx-auto bg-white shadow-2xl rounded-[40px] overflow-hidden min-h-screen border border-black/5">
|
|
508
508
|
<div v-for="(block, index) in page.content" :key="index" @click="selectedBlockIndex = index"
|
|
509
|
-
class="relative cursor-pointer" :class="{ 'ring-4 ring-
|
|
509
|
+
class="relative cursor-pointer" :class="{ 'ring-4 ring-[#3d35ff] ring-inset z-10': selectedBlockIndex === index }">
|
|
510
510
|
<component :is="resolveSection(block.type)" v-bind="resolvePropsForPreview(block.props)" />
|
|
511
511
|
</div>
|
|
512
512
|
</div>
|
|
513
513
|
</main>
|
|
514
514
|
|
|
515
515
|
<!-- ── Right Panel: Props / SEO ── -->
|
|
516
|
-
<aside :class="['bg-
|
|
516
|
+
<aside :class="['bg-white border-l z-50 transition-all duration-300 overflow-y-auto', isRightSidebarOpen ? 'w-80' : 'w-0']">
|
|
517
517
|
<div class="p-6 w-80">
|
|
518
518
|
|
|
519
519
|
<!-- Mode tabs + lang switcher -->
|
|
520
520
|
<div class="flex items-center justify-between mb-5 border-b pb-4">
|
|
521
|
-
<div class="flex gap-0.5 bg-
|
|
521
|
+
<div class="flex gap-0.5 bg-gray-100 p-0.5 rounded-lg">
|
|
522
522
|
<button @click="rightPanelMode = 'block'"
|
|
523
523
|
:disabled="selectedBlockIndex === null || page?.content_locked"
|
|
524
524
|
:class="[
|
|
525
525
|
'px-2.5 py-1 text-[7px] font-black uppercase rounded-md transition-all',
|
|
526
|
-
rightPanelMode === 'block' ? 'bg-
|
|
526
|
+
rightPanelMode === 'block' ? 'bg-white shadow text-[#3d35ff]' : 'text-gray-400',
|
|
527
527
|
selectedBlockIndex === null || page?.content_locked ? 'opacity-30 cursor-not-allowed' : ''
|
|
528
528
|
]">
|
|
529
529
|
Bloc
|
|
@@ -531,16 +531,16 @@ const savePage = async () => {
|
|
|
531
531
|
<button @click="rightPanelMode = 'seo'"
|
|
532
532
|
:class="[
|
|
533
533
|
'px-2.5 py-1 text-[7px] font-black uppercase rounded-md transition-all',
|
|
534
|
-
rightPanelMode === 'seo' ? 'bg-
|
|
534
|
+
rightPanelMode === 'seo' ? 'bg-white shadow text-[#3d35ff]' : 'text-gray-400'
|
|
535
535
|
]">
|
|
536
536
|
SEO
|
|
537
537
|
</button>
|
|
538
538
|
</div>
|
|
539
|
-
<div class="flex gap-0.5 bg-
|
|
539
|
+
<div class="flex gap-0.5 bg-gray-100 p-0.5 rounded-lg">
|
|
540
540
|
<button v-for="lang in LANGS" :key="lang" @click="editingLang = lang"
|
|
541
541
|
:class="[
|
|
542
542
|
'px-2 py-1 text-[7px] font-black uppercase rounded-md transition-all',
|
|
543
|
-
editingLang === lang ? 'bg-
|
|
543
|
+
editingLang === lang ? 'bg-white shadow text-[#3d35ff]' : 'text-gray-400'
|
|
544
544
|
]">
|
|
545
545
|
{{ langLabel(lang) }}
|
|
546
546
|
</button>
|
|
@@ -551,7 +551,7 @@ const savePage = async () => {
|
|
|
551
551
|
<div v-if="rightPanelMode === 'block' && selectedBlockIndex !== null">
|
|
552
552
|
|
|
553
553
|
<!-- Diff -->
|
|
554
|
-
<div v-if="blockDiffs.has(selectedBlockIndex)" class="mb-5 rounded-2xl overflow-hidden border border-amber-200 bg-
|
|
554
|
+
<div v-if="blockDiffs.has(selectedBlockIndex)" class="mb-5 rounded-2xl overflow-hidden border border-amber-200 bg-white">
|
|
555
555
|
<button @click="showDiff = !showDiff"
|
|
556
556
|
class="w-full flex items-center justify-between px-4 py-3 bg-amber-50 hover:bg-amber-100 transition-colors">
|
|
557
557
|
<span class="flex items-center gap-2 text-[10px] font-black text-amber-700 uppercase tracking-wider">
|
|
@@ -562,7 +562,7 @@ const savePage = async () => {
|
|
|
562
562
|
</button>
|
|
563
563
|
<div v-if="showDiff" class="divide-y divide-gray-50">
|
|
564
564
|
<div v-for="change in blockDiffs.get(selectedBlockIndex)" :key="change.field" class="px-4 py-3">
|
|
565
|
-
<p class="text-[9px] font-black uppercase tracking-widest text-
|
|
565
|
+
<p class="text-[9px] font-black uppercase tracking-widest text-gray-400 mb-2">{{ change.field }}</p>
|
|
566
566
|
<div class="flex items-start gap-2 mb-1.5">
|
|
567
567
|
<span class="w-1 self-stretch rounded-full bg-red-300 shrink-0"></span>
|
|
568
568
|
<p class="text-[11px] text-red-500 line-through leading-snug break-words min-w-0">{{ change.old }}</p>
|
|
@@ -578,40 +578,40 @@ const savePage = async () => {
|
|
|
578
578
|
<!-- AI Generate -->
|
|
579
579
|
<div v-if="aiEnabled" class="mb-5">
|
|
580
580
|
<div v-if="!showAiPrompt && !isGenerating">
|
|
581
|
-
<button @click="showAiPrompt = true" class="w-full py-2.5 rounded-xl border-2 border-dashed border-
|
|
581
|
+
<button @click="showAiPrompt = true" class="w-full py-2.5 rounded-xl border-2 border-dashed border-[#3d35ff]/30 text-[#3d35ff] text-[8px] font-black uppercase tracking-widest hover:border-[#3d35ff] hover:bg-[#3d35ff]/5 transition-all">
|
|
582
582
|
✦ Générer avec l'IA
|
|
583
583
|
</button>
|
|
584
584
|
</div>
|
|
585
|
-
<div v-else-if="isGenerating" class="py-2.5 rounded-xl bg-
|
|
585
|
+
<div v-else-if="isGenerating" class="py-2.5 rounded-xl bg-[#3d35ff]/5 border border-[#3d35ff]/20 text-center text-[8px] font-black uppercase tracking-widest text-[#3d35ff] animate-pulse">
|
|
586
586
|
Génération en cours...
|
|
587
587
|
</div>
|
|
588
|
-
<div v-else class="p-3 bg-
|
|
589
|
-
<label class="text-[8px] font-black uppercase text-
|
|
590
|
-
<textarea v-model="aiPromptText" rows="2" placeholder="Ex: service de création de sites web pour PME..." class="w-full bg-
|
|
588
|
+
<div v-else class="p-3 bg-[#3d35ff]/5 rounded-2xl border border-[#3d35ff]/20 space-y-2">
|
|
589
|
+
<label class="text-[8px] font-black uppercase text-[#3d35ff]/60 tracking-widest block">Décrivez votre contenu</label>
|
|
590
|
+
<textarea v-model="aiPromptText" rows="2" placeholder="Ex: service de création de sites web pour PME..." class="w-full bg-white rounded-xl p-2.5 text-[10px] outline-none ring-1 ring-[#3d35ff]/20 focus:ring-[#3d35ff] resize-none"></textarea>
|
|
591
591
|
<div class="flex gap-2">
|
|
592
592
|
<button @click="showAiPrompt = false;
|
|
593
|
-
aiPromptText = ''" class="flex-1 py-2 rounded-xl border border-
|
|
594
|
-
<button @click="generateWithAI" :disabled="!aiPromptText.trim()" class="flex-1 py-2 rounded-xl bg-
|
|
593
|
+
aiPromptText = ''" class="flex-1 py-2 rounded-xl border border-gray-200 text-[8px] font-black uppercase text-gray-400 hover:border-gray-300 transition-all">Annuler</button>
|
|
594
|
+
<button @click="generateWithAI" :disabled="!aiPromptText.trim()" class="flex-1 py-2 rounded-xl bg-[#3d35ff] text-white text-[8px] font-black uppercase tracking-widest disabled:opacity-40 transition-all active:scale-95">✦ Générer</button>
|
|
595
595
|
</div>
|
|
596
596
|
</div>
|
|
597
597
|
</div>
|
|
598
598
|
|
|
599
599
|
<!-- Anchor ID -->
|
|
600
|
-
<div class="mb-6 pb-6 border-b border-
|
|
601
|
-
<label class="text-[8px] font-black uppercase text-
|
|
602
|
-
<div class="flex items-center bg-
|
|
603
|
-
<span class="text-[9px] font-mono text-
|
|
600
|
+
<div class="mb-6 pb-6 border-b border-gray-50">
|
|
601
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">ID / Ancre</label>
|
|
602
|
+
<div class="flex items-center bg-gray-50 rounded-xl ring-1 ring-gray-100 focus-within:ring-[#3d35ff] overflow-hidden">
|
|
603
|
+
<span class="text-[9px] font-mono text-gray-300 pl-3 pr-1 shrink-0">#</span>
|
|
604
604
|
<input v-model="page.content[selectedBlockIndex].id" class="flex-1 bg-transparent p-3 text-[10px] font-mono outline-none" placeholder="mon-ancre" />
|
|
605
605
|
</div>
|
|
606
606
|
</div>
|
|
607
607
|
|
|
608
608
|
<!-- Group tabs -->
|
|
609
|
-
<div v-if="selectedCMSConfig?.groups" class="flex gap-1 bg-
|
|
609
|
+
<div v-if="selectedCMSConfig?.groups" class="flex gap-1 bg-gray-100 p-1 rounded-xl mb-6">
|
|
610
610
|
<button v-for="group in selectedCMSConfig.groups" :key="group.key"
|
|
611
611
|
@click="selectedFieldGroup = group.key"
|
|
612
612
|
:class="[
|
|
613
613
|
'flex-1 py-2 text-[8px] font-black uppercase tracking-widest rounded-lg transition-all',
|
|
614
|
-
selectedFieldGroup === group.key ? 'bg-
|
|
614
|
+
selectedFieldGroup === group.key ? 'bg-white shadow text-[#3d35ff]' : 'text-gray-400 hover:text-gray-600'
|
|
615
615
|
]">
|
|
616
616
|
{{ group.label }}
|
|
617
617
|
</button>
|
|
@@ -619,7 +619,7 @@ const savePage = async () => {
|
|
|
619
619
|
|
|
620
620
|
<!-- Fields -->
|
|
621
621
|
<div v-for="(field, name) in visibleFields" :key="name" class="mb-6">
|
|
622
|
-
<label class="text-[8px] font-black uppercase text-
|
|
622
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">{{ field.label }}</label>
|
|
623
623
|
|
|
624
624
|
<!-- Image -->
|
|
625
625
|
<div v-if="field.type === 'image'" class="space-y-2">
|
|
@@ -628,10 +628,10 @@ const savePage = async () => {
|
|
|
628
628
|
<button @click="page.content[selectedBlockIndex].props[name] = ''" class="absolute inset-0 bg-black/60 text-white text-[8px] font-black opacity-0 group-hover:opacity-100 transition-opacity">SUPPRIMER</button>
|
|
629
629
|
</div>
|
|
630
630
|
<div class="flex gap-2">
|
|
631
|
-
<button @click="triggerUpload(page.content[selectedBlockIndex].props, name)" class="flex-1 py-2.5 bg-
|
|
631
|
+
<button @click="triggerUpload(page.content[selectedBlockIndex].props, name)" class="flex-1 py-2.5 bg-gray-50 border-2 border-dashed rounded-xl text-[9px] font-black hover:bg-gray-100 transition-all" :disabled="isUploading">
|
|
632
632
|
{{ isUploading ? "ENVOI..." : "\u2191 UPLOAD" }}
|
|
633
633
|
</button>
|
|
634
|
-
<button @click="openGallery(page.content[selectedBlockIndex].props, name)" class="flex-1 py-2.5 bg-
|
|
634
|
+
<button @click="openGallery(page.content[selectedBlockIndex].props, name)" class="flex-1 py-2.5 bg-gray-50 border-2 border-dashed rounded-xl text-[9px] font-black hover:bg-gray-100 transition-all">
|
|
635
635
|
⊞ GALERIE
|
|
636
636
|
</button>
|
|
637
637
|
</div>
|
|
@@ -640,26 +640,26 @@ const savePage = async () => {
|
|
|
640
640
|
<!-- Text i18n -->
|
|
641
641
|
<template v-else-if="field.type === 'text'">
|
|
642
642
|
<div v-if="field.i18n" class="relative">
|
|
643
|
-
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-
|
|
644
|
-
<input :value="getI18nProp(name, editingLang)" @input="setI18nProp(name, editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" class="w-full bg-
|
|
643
|
+
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-[#3d35ff]/40">{{ langLabel(editingLang) }}</span>
|
|
644
|
+
<input :value="getI18nProp(name, editingLang)" @input="setI18nProp(name, editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]" />
|
|
645
645
|
</div>
|
|
646
646
|
<input v-else :value="page.content[selectedBlockIndex].props[name]" @input="page.content[selectedBlockIndex].props[name] = $event.target.value;
|
|
647
|
-
trigger()" class="w-full bg-
|
|
647
|
+
trigger()" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]" />
|
|
648
648
|
</template>
|
|
649
649
|
|
|
650
650
|
<!-- Textarea i18n -->
|
|
651
651
|
<template v-else-if="field.type === 'textarea'">
|
|
652
652
|
<div v-if="field.i18n" class="relative">
|
|
653
|
-
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-
|
|
654
|
-
<textarea :value="getI18nProp(name, editingLang)" @input="setI18nProp(name, editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" rows="3" class="w-full bg-
|
|
653
|
+
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-[#3d35ff]/40">{{ langLabel(editingLang) }}</span>
|
|
654
|
+
<textarea :value="getI18nProp(name, editingLang)" @input="setI18nProp(name, editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" rows="3" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]"></textarea>
|
|
655
655
|
</div>
|
|
656
656
|
<textarea v-else :value="page.content[selectedBlockIndex].props[name]" @input="page.content[selectedBlockIndex].props[name] = $event.target.value;
|
|
657
|
-
trigger()" rows="3" class="w-full bg-
|
|
657
|
+
trigger()" rows="3" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] outline-none ring-1 ring-gray-100"></textarea>
|
|
658
658
|
</template>
|
|
659
659
|
|
|
660
660
|
<!-- Select -->
|
|
661
661
|
<select v-else-if="field.type === 'select'" :value="page.content[selectedBlockIndex].props[name]" @change="page.content[selectedBlockIndex].props[name] = $event.target.value;
|
|
662
|
-
trigger()" class="w-full bg-
|
|
662
|
+
trigger()" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold ring-1 ring-gray-100 outline-none">
|
|
663
663
|
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
|
664
664
|
</select>
|
|
665
665
|
|
|
@@ -670,7 +670,7 @@ const savePage = async () => {
|
|
|
670
670
|
trigger()"
|
|
671
671
|
:class="[
|
|
672
672
|
'p-2.5 rounded-xl border-2 text-center transition-all',
|
|
673
|
-
page.content[selectedBlockIndex].props[name] === opt.value ? 'border-
|
|
673
|
+
page.content[selectedBlockIndex].props[name] === opt.value ? 'border-[#3d35ff] bg-blue-50/50 text-[#3d35ff]' : 'border-gray-100 text-gray-400 hover:border-gray-200'
|
|
674
674
|
]">
|
|
675
675
|
<span class="text-base block mb-1">{{ opt.icon }}</span>
|
|
676
676
|
<span class="text-[7px] font-black uppercase tracking-wide block leading-tight">{{ opt.label }}</span>
|
|
@@ -678,7 +678,7 @@ const savePage = async () => {
|
|
|
678
678
|
</div>
|
|
679
679
|
|
|
680
680
|
<!-- Color -->
|
|
681
|
-
<div v-else-if="field.type === 'color'" class="flex gap-2 bg-
|
|
681
|
+
<div v-else-if="field.type === 'color'" class="flex gap-2 bg-gray-50 p-2 rounded-xl ring-1 ring-gray-100">
|
|
682
682
|
<button v-for="c in ['#3d35ff', '#ffffff', '#000000', '#F9F9FB']" :key="c" @click="page.content[selectedBlockIndex].props[name] = c;
|
|
683
683
|
trigger()" class="w-5 h-5 rounded-full border border-black/5" :style="{ background: c }"></button>
|
|
684
684
|
<input :value="page.content[selectedBlockIndex].props[name]" @input="page.content[selectedBlockIndex].props[name] = $event.target.value;
|
|
@@ -687,7 +687,7 @@ const savePage = async () => {
|
|
|
687
687
|
|
|
688
688
|
<!-- List -->
|
|
689
689
|
<div v-else-if="field.type === 'list'" class="space-y-4">
|
|
690
|
-
<div v-for="(item, idx) in page.content[selectedBlockIndex].props[name] || []" :key="idx" class="p-4 bg-
|
|
690
|
+
<div v-for="(item, idx) in page.content[selectedBlockIndex].props[name] || []" :key="idx" class="p-4 bg-gray-50 rounded-2xl relative border">
|
|
691
691
|
<button @click="page.content[selectedBlockIndex].props[name].splice(idx, 1);
|
|
692
692
|
trigger()" class="absolute top-2 right-2 text-red-400 text-[8px]">✕</button>
|
|
693
693
|
<div v-for="(subField, subName) in field.itemFields" :key="subName" class="mb-2">
|
|
@@ -695,16 +695,16 @@ const savePage = async () => {
|
|
|
695
695
|
<div v-if="subField.type === 'image'" class="mt-1 space-y-1.5">
|
|
696
696
|
<img v-if="item[subName]" :src="item[subName]" class="w-full h-16 rounded-lg border object-cover" />
|
|
697
697
|
<div class="flex gap-1.5">
|
|
698
|
-
<button @click="triggerUpload(item, subName)" class="flex-1 text-[8px] font-black uppercase bg-
|
|
699
|
-
<button @click="openGallery(item, subName)" class="flex-1 text-[8px] font-black uppercase bg-
|
|
698
|
+
<button @click="triggerUpload(item, subName)" class="flex-1 text-[8px] font-black uppercase bg-white px-2 py-1.5 rounded-lg shadow-sm border">↑ Upload</button>
|
|
699
|
+
<button @click="openGallery(item, subName)" class="flex-1 text-[8px] font-black uppercase bg-white px-2 py-1.5 rounded-lg shadow-sm border">⊞ Galerie</button>
|
|
700
700
|
</div>
|
|
701
701
|
</div>
|
|
702
702
|
<select v-else-if="subField.type === 'select'" :value="item[subName]" @change="item[subName] = $event.target.value;
|
|
703
|
-
trigger()" class="w-full bg-
|
|
703
|
+
trigger()" class="w-full bg-white rounded-lg p-2 text-[10px] font-bold outline-none shadow-sm border">
|
|
704
704
|
<option v-for="opt in subField.options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
|
705
705
|
</select>
|
|
706
706
|
<template v-else-if="subField.i18n">
|
|
707
|
-
<input :value="getI18nVal(item[subName], editingLang)" @input="setI18nVal(item, subName, editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" :placeholder="langLabel(editingLang)" class="w-full bg-
|
|
707
|
+
<input :value="getI18nVal(item[subName], editingLang)" @input="setI18nVal(item, subName, editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" :placeholder="langLabel(editingLang)" class="w-full bg-white rounded-lg p-2 text-[10px] font-bold outline-none shadow-sm" />
|
|
708
708
|
</template>
|
|
709
709
|
<div v-else-if="subField.type === 'stringlist'" class="space-y-1 mt-1">
|
|
710
710
|
<div v-for="(feat, fi) in item[subName] || []" :key="fi" class="flex gap-1 items-center">
|
|
@@ -713,33 +713,33 @@ const savePage = async () => {
|
|
|
713
713
|
@input="setI18nVal(item[subName], fi, editingLang, $event.target.value)"
|
|
714
714
|
:dir="editingLang === 'ar' ? 'rtl' : 'ltr'"
|
|
715
715
|
:placeholder="langLabel(editingLang)"
|
|
716
|
-
class="flex-1 bg-
|
|
716
|
+
class="flex-1 bg-white rounded-lg p-2 text-[10px] font-bold outline-none shadow-sm" />
|
|
717
717
|
<button @click="item[subName].splice(fi, 1);
|
|
718
718
|
trigger()" class="text-red-400 text-[8px] px-1 shrink-0">✕</button>
|
|
719
719
|
</div>
|
|
720
720
|
<button @click="if (!item[subName]) item[subName] = [];
|
|
721
721
|
item[subName].push({ fr: '', ar: '', en: '' });
|
|
722
|
-
trigger()" class="w-full py-1.5 border border-dashed rounded-lg text-[8px] font-black text-
|
|
722
|
+
trigger()" class="w-full py-1.5 border border-dashed rounded-lg text-[8px] font-black text-gray-400 hover:border-gray-400 transition">+ Fonctionnalité</button>
|
|
723
723
|
</div>
|
|
724
724
|
<input v-else :value="item[subName]" @input="item[subName] = $event.target.value;
|
|
725
|
-
trigger()" class="w-full bg-
|
|
725
|
+
trigger()" class="w-full bg-white rounded-lg p-2 text-[10px] font-bold outline-none shadow-sm" />
|
|
726
726
|
</div>
|
|
727
727
|
</div>
|
|
728
728
|
<button @click="if (!page.content[selectedBlockIndex].props[name]) page.content[selectedBlockIndex].props[name] = [];
|
|
729
729
|
page.content[selectedBlockIndex].props[name].push({});
|
|
730
|
-
trigger()" class="w-full py-2 border-2 border-dashed rounded-xl text-[8px] font-black text-
|
|
730
|
+
trigger()" class="w-full py-2 border-2 border-dashed rounded-xl text-[8px] font-black text-gray-400">+ AJOUTER</button>
|
|
731
731
|
</div>
|
|
732
732
|
</div>
|
|
733
733
|
</div>
|
|
734
734
|
|
|
735
735
|
<!-- ── SEO CONFIG PANEL ── -->
|
|
736
736
|
<div v-else-if="rightPanelMode === 'seo'">
|
|
737
|
-
<h3 class="text-[9px] font-black text-
|
|
737
|
+
<h3 class="text-[9px] font-black text-gray-300 uppercase mb-4 italic tracking-widest">Config SEO</h3>
|
|
738
738
|
|
|
739
739
|
<!-- AI Audit -->
|
|
740
740
|
<div v-if="aiEnabled" class="mb-6">
|
|
741
741
|
<button @click="runSeoAudit" :disabled="isAuditing"
|
|
742
|
-
class="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-widest bg-
|
|
742
|
+
class="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-widest bg-[#3d35ff]/10 text-[#3d35ff] hover:bg-[#3d35ff] hover:text-white transition-all disabled:opacity-50">
|
|
743
743
|
<svg v-if="isAuditing" class="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/></svg>
|
|
744
744
|
<svg v-else class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
|
745
745
|
{{ isAuditing ? "Analyse..." : "Audit IA" }}
|
|
@@ -748,7 +748,7 @@ const savePage = async () => {
|
|
|
748
748
|
|
|
749
749
|
<!-- Audit results -->
|
|
750
750
|
<div v-if="aiEnabled && seoAudit" class="mb-6 space-y-3">
|
|
751
|
-
<div class="flex items-center gap-3 p-3 bg-
|
|
751
|
+
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-xl">
|
|
752
752
|
<div :class="[
|
|
753
753
|
'w-11 h-11 rounded-xl flex items-center justify-center text-sm font-black shrink-0',
|
|
754
754
|
seoAudit.score >= 75 ? 'bg-green-100 text-green-700' : seoAudit.score >= 50 ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-600'
|
|
@@ -756,15 +756,15 @@ const savePage = async () => {
|
|
|
756
756
|
{{ seoAudit.score }}
|
|
757
757
|
</div>
|
|
758
758
|
<div>
|
|
759
|
-
<p class="text-[9px] font-black uppercase tracking-widest text-
|
|
759
|
+
<p class="text-[9px] font-black uppercase tracking-widest text-gray-400">Score SEO</p>
|
|
760
760
|
<p :class="['text-[11px] font-black', seoAudit.score >= 75 ? 'text-green-600' : seoAudit.score >= 50 ? 'text-amber-600' : 'text-red-500']">
|
|
761
761
|
{{ seoAudit.score >= 75 ? "Bon" : seoAudit.score >= 50 ? "\xC0 am\xE9liorer" : "Insuffisant" }}
|
|
762
762
|
</p>
|
|
763
763
|
</div>
|
|
764
|
-
<button @click="applySeoAudit" class="ml-auto px-3 py-1.5 bg-
|
|
764
|
+
<button @click="applySeoAudit" class="ml-auto px-3 py-1.5 bg-[#3d35ff] text-white rounded-lg text-[9px] font-black uppercase tracking-wide hover:bg-[#2f28cc] transition shrink-0">Appliquer</button>
|
|
765
765
|
</div>
|
|
766
766
|
<div v-if="seoAudit.issues?.length" class="space-y-1.5">
|
|
767
|
-
<p class="text-[8px] font-black uppercase tracking-widest text-
|
|
767
|
+
<p class="text-[8px] font-black uppercase tracking-widest text-gray-400">Problèmes</p>
|
|
768
768
|
<div v-for="(issue, i) in seoAudit.issues" :key="i"
|
|
769
769
|
:class="[
|
|
770
770
|
'flex items-start gap-2 p-2.5 rounded-lg text-[10px]',
|
|
@@ -775,89 +775,89 @@ const savePage = async () => {
|
|
|
775
775
|
</div>
|
|
776
776
|
</div>
|
|
777
777
|
<div v-if="seoAudit.suggestions?.length" class="space-y-1.5">
|
|
778
|
-
<p class="text-[8px] font-black uppercase tracking-widest text-
|
|
779
|
-
<div v-for="(tip, i) in seoAudit.suggestions" :key="i" class="flex items-start gap-2 p-2.5 bg-
|
|
778
|
+
<p class="text-[8px] font-black uppercase tracking-widest text-gray-400">Recommandations</p>
|
|
779
|
+
<div v-for="(tip, i) in seoAudit.suggestions" :key="i" class="flex items-start gap-2 p-2.5 bg-[#3d35ff]/5 rounded-lg text-[10px] text-[#3d35ff]">
|
|
780
780
|
<span class="shrink-0 font-black mt-0.5">→</span>{{ tip }}
|
|
781
781
|
</div>
|
|
782
782
|
</div>
|
|
783
783
|
<div v-if="seoAudit.keywords?.length">
|
|
784
|
-
<p class="text-[8px] font-black uppercase tracking-widest text-
|
|
784
|
+
<p class="text-[8px] font-black uppercase tracking-widest text-gray-400 mb-1.5">Mots-clés suggérés</p>
|
|
785
785
|
<div class="flex flex-wrap gap-1">
|
|
786
|
-
<span v-for="kw in seoAudit.keywords" :key="kw" class="px-2 py-1 bg-
|
|
786
|
+
<span v-for="kw in seoAudit.keywords" :key="kw" class="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-[9px] font-semibold">{{ kw }}</span>
|
|
787
787
|
</div>
|
|
788
788
|
</div>
|
|
789
|
-
<div class="border-t border-
|
|
790
|
-
<p class="text-[8px] font-black uppercase tracking-widest text-
|
|
791
|
-
<p class="text-[10px] text-
|
|
792
|
-
<p class="text-[8px] font-black uppercase tracking-widest text-
|
|
793
|
-
<p class="text-[10px] text-
|
|
789
|
+
<div class="border-t border-gray-100 pt-3">
|
|
790
|
+
<p class="text-[8px] font-black uppercase tracking-widest text-gray-400 mb-1">Meta title suggéré</p>
|
|
791
|
+
<p class="text-[10px] text-gray-700 bg-gray-50 rounded-lg p-2">{{ seoAudit.meta_title?.fr }}</p>
|
|
792
|
+
<p class="text-[8px] font-black uppercase tracking-widest text-gray-400 mb-1 mt-2">Meta description suggérée</p>
|
|
793
|
+
<p class="text-[10px] text-gray-500 bg-gray-50 rounded-lg p-2">{{ seoAudit.meta_description?.fr }}</p>
|
|
794
794
|
</div>
|
|
795
795
|
</div>
|
|
796
796
|
|
|
797
|
-
<div class="border-t border-
|
|
797
|
+
<div class="border-t border-gray-100 pt-4 mb-5"></div>
|
|
798
798
|
|
|
799
799
|
<div class="mb-5">
|
|
800
|
-
<label class="text-[8px] font-black uppercase text-
|
|
801
|
-
<select v-model="page.type" class="w-full bg-
|
|
800
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">Type de page</label>
|
|
801
|
+
<select v-model="page.type" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]">
|
|
802
802
|
<option v-for="opt in pageTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
|
803
803
|
</select>
|
|
804
804
|
</div>
|
|
805
805
|
|
|
806
806
|
<div class="mb-5">
|
|
807
|
-
<label class="text-[8px] font-black uppercase text-
|
|
808
|
-
<input v-model="page.title" class="w-full bg-
|
|
807
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">Titre de la page</label>
|
|
808
|
+
<input v-model="page.title" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]" placeholder="Nom interne de la page" />
|
|
809
809
|
</div>
|
|
810
810
|
|
|
811
811
|
<div class="mb-5">
|
|
812
|
-
<label class="text-[8px] font-black uppercase text-
|
|
813
|
-
<div class="flex items-center bg-
|
|
814
|
-
<span class="text-[9px] font-mono text-
|
|
812
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">URL (Slug)</label>
|
|
813
|
+
<div class="flex items-center bg-gray-50 rounded-xl ring-1 ring-gray-100 focus-within:ring-[#3d35ff] overflow-hidden">
|
|
814
|
+
<span class="text-[9px] font-mono text-gray-300 pl-3 pr-1 shrink-0">/</span>
|
|
815
815
|
<input v-model="page.slug" class="flex-1 bg-transparent p-3 text-[10px] font-mono outline-none" placeholder="mon-url" />
|
|
816
816
|
</div>
|
|
817
817
|
</div>
|
|
818
818
|
|
|
819
819
|
<div class="mb-5">
|
|
820
|
-
<label class="text-[8px] font-black uppercase text-
|
|
820
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">Meta Title</label>
|
|
821
821
|
<div class="relative">
|
|
822
|
-
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-
|
|
823
|
-
<input :value="getSeoI18n('meta_title', editingLang)" @input="setSeoI18n('meta_title', editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" placeholder="Title de la page..." class="w-full bg-
|
|
822
|
+
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-[#3d35ff]/40">{{ langLabel(editingLang) }}</span>
|
|
823
|
+
<input :value="getSeoI18n('meta_title', editingLang)" @input="setSeoI18n('meta_title', editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" placeholder="Title de la page..." class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]" />
|
|
824
824
|
</div>
|
|
825
825
|
</div>
|
|
826
826
|
|
|
827
827
|
<div class="mb-5">
|
|
828
|
-
<label class="text-[8px] font-black uppercase text-
|
|
828
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">Meta Description</label>
|
|
829
829
|
<div class="relative">
|
|
830
|
-
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-
|
|
831
|
-
<textarea :value="getSeoI18n('meta_description', editingLang)" @input="setSeoI18n('meta_description', editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" rows="4" placeholder="Description courte de la page..." class="w-full bg-
|
|
830
|
+
<span class="absolute -top-1 right-0 text-[6px] font-black uppercase text-[#3d35ff]/40">{{ langLabel(editingLang) }}</span>
|
|
831
|
+
<textarea :value="getSeoI18n('meta_description', editingLang)" @input="setSeoI18n('meta_description', editingLang, $event.target.value)" :dir="editingLang === 'ar' ? 'rtl' : 'ltr'" rows="4" placeholder="Description courte de la page..." class="w-full bg-gray-50 rounded-xl p-3 text-[10px] outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]"></textarea>
|
|
832
832
|
</div>
|
|
833
833
|
</div>
|
|
834
834
|
|
|
835
835
|
<div class="mb-5">
|
|
836
|
-
<label class="text-[8px] font-black uppercase text-
|
|
837
|
-
<input :value="page?.seo_config?.og_title ?? ''" @input="updateSeoField('og_title', $event.target.value)" class="w-full bg-
|
|
836
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">OG Title <span class="text-gray-300 normal-case font-medium">(défaut: meta title)</span></label>
|
|
837
|
+
<input :value="page?.seo_config?.og_title ?? ''" @input="updateSeoField('og_title', $event.target.value)" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]" />
|
|
838
838
|
</div>
|
|
839
839
|
|
|
840
840
|
<div class="mb-5">
|
|
841
|
-
<label class="text-[8px] font-black uppercase text-
|
|
842
|
-
<textarea :value="page?.seo_config?.og_description ?? ''" @input="updateSeoField('og_description', $event.target.value)" rows="3" class="w-full bg-
|
|
841
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">OG Description</label>
|
|
842
|
+
<textarea :value="page?.seo_config?.og_description ?? ''" @input="updateSeoField('og_description', $event.target.value)" rows="3" class="w-full bg-gray-50 rounded-xl p-3 text-[10px] outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]"></textarea>
|
|
843
843
|
</div>
|
|
844
844
|
|
|
845
845
|
<div class="mb-5">
|
|
846
|
-
<label class="text-[8px] font-black uppercase text-
|
|
847
|
-
<input :value="page?.seo_config?.og_image ?? ''" @input="updateSeoField('og_image', $event.target.value)" placeholder="https://..." class="w-full bg-
|
|
846
|
+
<label class="text-[8px] font-black uppercase text-gray-400 mb-2 block tracking-widest">OG Image (URL)</label>
|
|
847
|
+
<input :value="page?.seo_config?.og_image ?? ''" @input="updateSeoField('og_image', $event.target.value)" placeholder="https://..." class="w-full bg-gray-50 rounded-xl p-3 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]" />
|
|
848
848
|
</div>
|
|
849
849
|
|
|
850
|
-
<div class="flex items-center justify-between p-3 bg-
|
|
851
|
-
<label class="text-[8px] font-black uppercase text-
|
|
850
|
+
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-xl ring-1 ring-gray-100">
|
|
851
|
+
<label class="text-[8px] font-black uppercase text-gray-400 tracking-widest">No Index</label>
|
|
852
852
|
<button @click="updateSeoField('no_index', !page?.seo_config?.no_index)"
|
|
853
|
-
:class="['w-9 h-5 rounded-full transition-colors relative', page?.seo_config?.no_index ? 'bg-
|
|
854
|
-
<span :class="['absolute top-0.5 w-4 h-4 bg-
|
|
853
|
+
:class="['w-9 h-5 rounded-full transition-colors relative', page?.seo_config?.no_index ? 'bg-[#3d35ff]' : 'bg-gray-200']">
|
|
854
|
+
<span :class="['absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform', page?.seo_config?.no_index ? 'translate-x-4' : 'translate-x-0.5']"></span>
|
|
855
855
|
</button>
|
|
856
856
|
</div>
|
|
857
857
|
</div>
|
|
858
858
|
|
|
859
859
|
<!-- Empty state -->
|
|
860
|
-
<div v-else class="text-center py-12 text-
|
|
860
|
+
<div v-else class="text-center py-12 text-gray-300">
|
|
861
861
|
<p class="text-[9px] font-black uppercase tracking-widest">Sélectionnez un bloc</p>
|
|
862
862
|
<p class="text-[8px] mt-2 opacity-60">ou passez en mode SEO →</p>
|
|
863
863
|
</div>
|
|
@@ -868,21 +868,21 @@ const savePage = async () => {
|
|
|
868
868
|
|
|
869
869
|
<!-- ── Gallery Modal ── -->
|
|
870
870
|
<div v-if="showGallery" class="fixed inset-0 bg-black/90 backdrop-blur-xl z-[110] flex items-center justify-center p-6" @click.self="showGallery = false">
|
|
871
|
-
<div class="bg-
|
|
871
|
+
<div class="bg-white max-w-4xl w-full rounded-[32px] flex flex-col overflow-hidden shadow-2xl" style="max-height:85vh" @click.stop>
|
|
872
872
|
<div class="flex items-center gap-4 px-8 py-5 border-b shrink-0">
|
|
873
|
-
<h2 class="text-[11px] font-black uppercase tracking-[0.4em] text-
|
|
873
|
+
<h2 class="text-[11px] font-black uppercase tracking-[0.4em] text-[#3d35ff] shrink-0">Galerie</h2>
|
|
874
874
|
<div class="flex-1 relative">
|
|
875
|
-
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-
|
|
876
|
-
<input v-model="gallerySearch" type="text" placeholder="Rechercher une image..." class="w-full bg-
|
|
875
|
+
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-300 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-4.35-4.35M17 11A6 6 0 115 11a6 6 0 0112 0z"/></svg>
|
|
876
|
+
<input v-model="gallerySearch" type="text" placeholder="Rechercher une image..." class="w-full bg-gray-50 rounded-xl pl-9 pr-4 py-2 text-[10px] font-bold outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff]" />
|
|
877
877
|
</div>
|
|
878
|
-
<button @click="showGallery = false" class="text-
|
|
878
|
+
<button @click="showGallery = false" class="text-gray-400 hover:text-black transition-colors text-lg leading-none shrink-0">✕</button>
|
|
879
879
|
</div>
|
|
880
880
|
<div class="flex-1 overflow-y-auto p-6">
|
|
881
|
-
<div v-if="isLoadingGallery" class="flex items-center justify-center py-20 text-
|
|
882
|
-
<div v-else-if="filteredGallery.length === 0" class="flex items-center justify-center py-20 text-
|
|
881
|
+
<div v-if="isLoadingGallery" class="flex items-center justify-center py-20 text-gray-300 text-[9px] font-black uppercase tracking-widest">Chargement...</div>
|
|
882
|
+
<div v-else-if="filteredGallery.length === 0" class="flex items-center justify-center py-20 text-gray-300 text-[9px] font-black uppercase tracking-widest">Aucune image trouvée</div>
|
|
883
883
|
<div v-else class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3">
|
|
884
884
|
<button v-for="img in filteredGallery" :key="img.url" @click="selectGalleryImage(img.url)"
|
|
885
|
-
class="group relative aspect-square rounded-xl overflow-hidden border-2 border-transparent hover:border-
|
|
885
|
+
class="group relative aspect-square rounded-xl overflow-hidden border-2 border-transparent hover:border-[#3d35ff] transition-all focus:outline-none">
|
|
886
886
|
<img :src="img.url" :alt="img.name" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
|
887
887
|
<div class="absolute bottom-0 inset-x-0 bg-black/60 px-2 py-1 translate-y-full group-hover:translate-y-0 transition-transform duration-200">
|
|
888
888
|
<p class="text-white text-[7px] font-bold truncate">{{ img.name }}</p>
|
|
@@ -895,16 +895,16 @@ const savePage = async () => {
|
|
|
895
895
|
|
|
896
896
|
<!-- ── Add Section Modal ── -->
|
|
897
897
|
<div v-if="showAddModal" class="fixed inset-0 bg-black/95 backdrop-blur-xl z-[100] flex items-center justify-center p-6" @click.self="showAddModal = false">
|
|
898
|
-
<div class="bg-
|
|
899
|
-
<h2 class="text-[12px] font-black uppercase text-center mb-12 tracking-[0.4em] italic text-
|
|
898
|
+
<div class="bg-white max-w-4xl w-full rounded-[40px] p-12 shadow-2xl overflow-y-auto max-h-[85vh]">
|
|
899
|
+
<h2 class="text-[12px] font-black uppercase text-center mb-12 tracking-[0.4em] italic text-[#3d35ff]">Nouvelle Section</h2>
|
|
900
900
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
|
901
901
|
<button v-for="(conf, type) in CMS_COMPONENTS" :key="type"
|
|
902
902
|
@click="page.content.push({ type, props: JSON.parse(JSON.stringify(adminSettings.applyPresets(type, conf.defaultProps))) });
|
|
903
903
|
showAddModal = false;
|
|
904
904
|
selectedBlockIndex = page.content.length - 1"
|
|
905
|
-
class="flex flex-col items-center gap-6 p-8 border-2 border-
|
|
905
|
+
class="flex flex-col items-center gap-6 p-8 border-2 border-gray-50 rounded-[40px] hover:border-[#3d35ff] transition-all group active:scale-95">
|
|
906
906
|
<span class="text-6xl group-hover:scale-110 transition-transform">{{ conf.icon }}</span>
|
|
907
|
-
<span class="text-[10px] font-black uppercase tracking-widest text-
|
|
907
|
+
<span class="text-[10px] font-black uppercase tracking-widest text-gray-900">{{ conf.label }}</span>
|
|
908
908
|
</button>
|
|
909
909
|
</div>
|
|
910
910
|
</div>
|