cinqcinqdev-seo 0.1.15 → 0.1.16
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 +554 -113
- package/dist/module.d.mts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -6
- package/dist/runtime/assets/admin-tw.css +1 -1
- package/dist/runtime/components/admin/Navbar.vue +6 -5
- package/dist/runtime/components/admin/Sidebar.vue +21 -8
- package/dist/runtime/components/sections/ContactForm.vue +14 -10
- package/dist/runtime/components/sections/FAQ.vue +10 -6
- package/dist/runtime/components/sections/Features.vue +10 -5
- package/dist/runtime/components/sections/HeroSection.vue +12 -8
- package/dist/runtime/components/sections/Pricing.vue +8 -5
- package/dist/runtime/components/sections/Process.vue +9 -4
- package/dist/runtime/components/sections/ServiceGrid.vue +10 -5
- package/dist/runtime/components/sections/Testimonials.vue +12 -7
- package/dist/runtime/components/sections/TextVisual.vue +8 -3
- package/dist/runtime/composables/useAdminBasePath.d.ts +1 -0
- package/dist/runtime/composables/useAdminBasePath.js +5 -0
- package/dist/runtime/composables/useAdminSections.d.ts +0 -7
- package/dist/runtime/composables/useAdminSections.js +68 -17
- package/dist/runtime/composables/useAdminSettings.d.ts +31 -0
- package/dist/runtime/composables/useAdminSettings.js +31 -0
- package/dist/runtime/middleware/admin-auth.js +3 -2
- package/dist/runtime/pages/admin/editor/[id].vue +10 -5
- package/dist/runtime/pages/admin/index.vue +3 -1
- package/dist/runtime/pages/admin/pages/[type].vue +5 -3
- package/dist/runtime/pages/admin/personas/[id].vue +3 -2
- package/dist/runtime/pages/admin/personas/index.vue +3 -2
- package/dist/runtime/pages/admin/setup.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/setup.vue +220 -0
- package/dist/runtime/pages/admin/setup.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/strategies/[id].vue +3 -2
- package/dist/runtime/pages/admin/strategies/index.vue +3 -2
- package/dist/runtime/server/api/ai/generate.post.js +8 -3
- package/dist/runtime/server/api/ai/seo-audit.post.js +8 -3
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.88] mb-6" :style="{ fontSize: titleSize === 'sm' ? 'clamp(2.5rem,5vw,4rem)' : titleSize === 'lg' ? 'clamp(3.5rem,9vw,8rem)' : 'clamp(3rem,7vw,6rem)' }">{{ title }}</component>
|
|
11
11
|
<p class="text-sm leading-relaxed max-w-lg mx-auto mb-10" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
12
12
|
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-8 py-3.5 rounded-full text-[11px] font-black uppercase tracking-widest transition-all hover:gap-5 active:scale-95" :style="{ backgroundColor: textColor, color: bgColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
13
|
-
<div v-if="image" class="mt-14 mx-auto aspect-video
|
|
13
|
+
<div v-if="image" :class="['mt-14 mx-auto aspect-video overflow-hidden max-w-3xl', cardRound]">
|
|
14
14
|
<img :src="image" class="w-full h-full object-cover" />
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<p class="text-sm leading-relaxed mb-10 max-w-sm" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
26
26
|
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-7 py-3 rounded-full text-[11px] font-black uppercase tracking-widest transition-all hover:gap-5 active:scale-95" :style="{ backgroundColor: textColor, color: bgColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
27
27
|
</div>
|
|
28
|
-
<div class="aspect-[4/3]
|
|
28
|
+
<div :class="['aspect-[4/3] overflow-hidden', cardRound]">
|
|
29
29
|
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
|
30
30
|
<div v-else class="w-full h-full" :style="{ backgroundColor: textColor + '06' }"></div>
|
|
31
31
|
</div>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<!-- BENTO -->
|
|
36
36
|
<div v-else-if="layout === 'bento'" class="p-3 md:p-5">
|
|
37
37
|
<div class="max-w-6xl mx-auto grid grid-cols-12 gap-3">
|
|
38
|
-
<div class="col-span-12 md:col-span-7
|
|
38
|
+
<div :class="['col-span-12 md:col-span-7 p-8 md:p-10 border flex flex-col justify-between min-h-[320px]', cardRound]" :style="{ borderColor: textColor + '10', backgroundColor: textColor + '04' }">
|
|
39
39
|
<span v-if="badge" class="text-[9px] font-black uppercase tracking-[0.4em] opacity-30">{{ badge }}</span>
|
|
40
40
|
<div>
|
|
41
41
|
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.88] mb-4" :style="{ fontSize: 'clamp(2rem,4.5vw,3.8rem)' }">{{ title }}</component>
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-all hover:gap-5 active:scale-95" :style="{ backgroundColor: textColor, color: bgColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
|
-
<div class="col-span-12 md:col-span-5
|
|
46
|
+
<div :class="['col-span-12 md:col-span-5 overflow-hidden min-h-[260px] md:min-h-[320px]', cardRound]">
|
|
47
47
|
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
|
48
48
|
<div v-else class="w-full h-full" :style="{ backgroundColor: textColor + '08' }"></div>
|
|
49
49
|
</div>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<span class="text-[9px] font-black uppercase tracking-[0.5em] opacity-20">{{ ctaText }}</span>
|
|
77
77
|
</div>
|
|
78
78
|
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.85] mb-10" :style="{ fontSize: 'clamp(3.5rem,10vw,9rem)' }">{{ title }}</component>
|
|
79
|
-
<div v-if="image" class="mb-10 overflow-hidden
|
|
79
|
+
<div v-if="image" :class="['mb-10 overflow-hidden aspect-video', cardRound]">
|
|
80
80
|
<img :src="image" class="w-full h-full object-cover" />
|
|
81
81
|
</div>
|
|
82
82
|
<div class="flex flex-col md:flex-row items-start md:items-end justify-between gap-6 border-t pt-8" :style="{ borderColor: textColor + '10' }">
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
</div>
|
|
96
96
|
<div class="flex flex-col md:flex-row md:items-end gap-8 mb-8">
|
|
97
97
|
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.85] flex-1" :style="{ fontSize: 'clamp(3rem,9vw,9rem)' }">{{ title }}</component>
|
|
98
|
-
<div v-if="image" class="md:w-56 md:shrink-0 aspect-[3/4]
|
|
98
|
+
<div v-if="image" :class="['md:w-56 md:shrink-0 aspect-[3/4] overflow-hidden', cardRound]">
|
|
99
99
|
<img :src="image" class="w-full h-full object-cover" />
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
@@ -110,7 +110,8 @@
|
|
|
110
110
|
</template>
|
|
111
111
|
|
|
112
112
|
<script setup>
|
|
113
|
-
|
|
113
|
+
import { computed } from "vue";
|
|
114
|
+
const props = defineProps({
|
|
114
115
|
layout: { type: String, default: "centered" },
|
|
115
116
|
animation: { type: String, default: "fadeUp" },
|
|
116
117
|
titleSize: { type: String, default: "md" },
|
|
@@ -124,6 +125,9 @@ defineProps({
|
|
|
124
125
|
imageStyle: { type: String, default: "cover" },
|
|
125
126
|
imagePosition: { type: String, default: "center" },
|
|
126
127
|
bgColor: { type: String, default: "#3d35ff" },
|
|
127
|
-
textColor: { type: String, default: "#ffffff" }
|
|
128
|
+
textColor: { type: String, default: "#ffffff" },
|
|
129
|
+
spacing: { type: String, default: "normal" },
|
|
130
|
+
radius: { type: String, default: "rounded" }
|
|
128
131
|
});
|
|
132
|
+
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
129
133
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="
|
|
2
|
+
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
3
|
<div class="max-w-6xl mx-auto">
|
|
4
4
|
|
|
5
5
|
<div v-if="title || description" class="mb-12 text-center max-w-2xl mx-auto">
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
<!-- CARDS -->
|
|
12
12
|
<div v-if="layout === 'cards'" class="grid grid-cols-1 md:grid-cols-3 gap-6 items-end">
|
|
13
13
|
<div v-for="(plan, i) in items" :key="i"
|
|
14
|
-
class="
|
|
15
|
-
:class="plan.highlighted ? 'md:-translate-y-4 shadow-2xl' : ''"
|
|
14
|
+
:class="['border p-8 flex flex-col transition-all duration-300 relative', cardRound, plan.highlighted ? 'md:-translate-y-4 shadow-2xl' : '']"
|
|
16
15
|
:style="plan.highlighted ? { backgroundColor: textColor, color: bgColor, borderColor: textColor } : { backgroundColor: textColor + '04', borderColor: textColor + '12' }">
|
|
17
16
|
<div v-if="plan.highlighted" class="absolute -top-3 left-1/2 -translate-x-1/2">
|
|
18
17
|
<span class="px-4 py-1 rounded-full text-[9px] font-black uppercase tracking-widest" :style="{ backgroundColor: bgColor, color: textColor }">Populaire</span>
|
|
@@ -63,7 +62,7 @@
|
|
|
63
62
|
<thead>
|
|
64
63
|
<tr class="border-b" :style="{ borderColor: textColor + '12' }">
|
|
65
64
|
<th class="text-left py-4 pr-6 font-black uppercase tracking-widest text-[10px]" :style="{ opacity: 0.4 }">Plan</th>
|
|
66
|
-
<th v-for="(plan, i) in items" :key="i" class="text-center py-4 px-4 font-black uppercase tracking-tight text-sm" :
|
|
65
|
+
<th v-for="(plan, i) in items" :key="i" class="text-center py-4 px-4 font-black uppercase tracking-tight text-sm" :style="plan.highlighted ? { color: textColor } : { opacity: 0.7 }">
|
|
67
66
|
{{ plan.name }}<br/><span class="text-2xl font-black tracking-tighter">{{ plan.price }}</span>
|
|
68
67
|
</th>
|
|
69
68
|
</tr>
|
|
@@ -94,8 +93,12 @@ const props = defineProps({
|
|
|
94
93
|
description: { type: String, default: "" },
|
|
95
94
|
items: { type: Array, default: () => [] },
|
|
96
95
|
bgColor: { type: String, default: "#ffffff" },
|
|
97
|
-
textColor: { type: String, default: "#000000" }
|
|
96
|
+
textColor: { type: String, default: "#000000" },
|
|
97
|
+
spacing: { type: String, default: "normal" },
|
|
98
|
+
radius: { type: String, default: "rounded" }
|
|
98
99
|
});
|
|
100
|
+
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
101
|
+
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
99
102
|
const allFeatures = computed(() => {
|
|
100
103
|
const set = /* @__PURE__ */ new Set();
|
|
101
104
|
props.items.forEach((p) => (p.features || []).forEach((f) => set.add(typeof f === "object" ? f.fr || JSON.stringify(f) : f)));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="
|
|
2
|
+
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
3
|
<div class="max-w-6xl mx-auto">
|
|
4
4
|
|
|
5
5
|
<div v-if="title || description" class="mb-12" :class="['horizontal','cards'].includes(layout) ? 'text-center max-w-2xl mx-auto' : 'max-w-xl'">
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
<!-- CARDS -->
|
|
38
38
|
<div v-else-if="layout === 'cards'" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
|
39
|
-
<div v-for="(step, i) in items" :key="i" class="p-7
|
|
39
|
+
<div v-for="(step, i) in items" :key="i" :class="['p-7 border', cardRound]" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
|
|
40
40
|
<div class="w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-black mb-5" :style="{ backgroundColor: textColor, color: bgColor }">
|
|
41
41
|
{{ step.icon || String(i+1).padStart(2,'0') }}
|
|
42
42
|
</div>
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
</template>
|
|
77
77
|
|
|
78
78
|
<script setup>
|
|
79
|
-
|
|
79
|
+
import { computed } from "vue";
|
|
80
|
+
const props = defineProps({
|
|
80
81
|
layout: { type: String, default: "steps" },
|
|
81
82
|
title: { type: String, default: "How it works" },
|
|
82
83
|
titleTag: { type: String, default: "h2" },
|
|
@@ -85,6 +86,10 @@ defineProps({
|
|
|
85
86
|
itemTag: { type: String, default: "h3" },
|
|
86
87
|
items: { type: Array, default: () => [] },
|
|
87
88
|
bgColor: { type: String, default: "#ffffff" },
|
|
88
|
-
textColor: { type: String, default: "#000000" }
|
|
89
|
+
textColor: { type: String, default: "#000000" },
|
|
90
|
+
spacing: { type: String, default: "normal" },
|
|
91
|
+
radius: { type: String, default: "rounded" }
|
|
89
92
|
});
|
|
93
|
+
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
94
|
+
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
90
95
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="
|
|
2
|
+
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
3
|
<div class="max-w-6xl mx-auto">
|
|
4
4
|
|
|
5
5
|
<div v-if="title || description" class="mb-12" :class="headerAlign === 'center' ? 'text-center max-w-2xl mx-auto' : 'max-w-xl'">
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<!-- GRID -->
|
|
19
19
|
<div v-if="layout === 'grid'" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
20
20
|
<div v-for="(service, idx) in items" :key="idx"
|
|
21
|
-
class="group relative p-8
|
|
21
|
+
:class="['group relative p-8 border transition-all duration-300 overflow-hidden hover:-translate-y-1 hover:shadow-lg', cardRound]"
|
|
22
22
|
:style="{ backgroundColor: textColor + '04', borderColor: textColor + '12' }">
|
|
23
23
|
<span class="absolute -top-6 -right-2 text-[100px] font-black italic opacity-[0.04] select-none">{{ String(idx+1).padStart(2,'0') }}</span>
|
|
24
24
|
<img v-if="service.image" :src="service.image" class="w-10 h-10 object-contain mb-5 rounded-xl" />
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<!-- CARDS -->
|
|
47
47
|
<div v-else-if="layout === 'cards'" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
48
48
|
<div v-for="(service, idx) in items" :key="idx"
|
|
49
|
-
class="flex gap-6 p-7
|
|
49
|
+
:class="['flex gap-6 p-7 border transition-all hover:shadow-md', cardRound]"
|
|
50
50
|
:style="{ backgroundColor: textColor + '03', borderColor: textColor + '10' }">
|
|
51
51
|
<div class="w-12 h-12 rounded-2xl flex items-center justify-center shrink-0" :style="{ backgroundColor: textColor + '10' }">
|
|
52
52
|
<img v-if="service.image" :src="service.image" class="w-7 h-7 object-contain" />
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
</template>
|
|
77
77
|
|
|
78
78
|
<script setup>
|
|
79
|
-
|
|
79
|
+
import { computed } from "vue";
|
|
80
|
+
const props = defineProps({
|
|
80
81
|
layout: { type: String, default: "grid" },
|
|
81
82
|
title: { type: String, default: "Our Services" },
|
|
82
83
|
titleTag: { type: String, default: "h2" },
|
|
@@ -87,6 +88,10 @@ defineProps({
|
|
|
87
88
|
items: { type: Array, default: () => [] },
|
|
88
89
|
ctas: { type: Array, default: () => [] },
|
|
89
90
|
bgColor: { type: String, default: "#ffffff" },
|
|
90
|
-
textColor: { type: String, default: "#000000" }
|
|
91
|
+
textColor: { type: String, default: "#000000" },
|
|
92
|
+
spacing: { type: String, default: "normal" },
|
|
93
|
+
radius: { type: String, default: "rounded" }
|
|
91
94
|
});
|
|
95
|
+
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
96
|
+
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
92
97
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="
|
|
2
|
+
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
3
|
<div class="max-w-6xl mx-auto">
|
|
4
4
|
|
|
5
5
|
<div v-if="title || description" class="mb-12 max-w-xl">
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<!-- GRID -->
|
|
12
12
|
<div v-if="layout === 'grid'" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
13
|
-
<div v-for="(item, i) in items" :key="i" class="flex flex-col p-7
|
|
13
|
+
<div v-for="(item, i) in items" :key="i" :class="['flex flex-col p-7 border transition-all hover:-translate-y-1', cardRound]" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
|
|
14
14
|
<div v-if="item.rating" class="flex gap-0.5 mb-4">
|
|
15
15
|
<span v-for="s in 5" :key="s" class="text-xs" :style="{ opacity: s <= item.rating ? 1 : 0.15 }">★</span>
|
|
16
16
|
</div>
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
<!-- FEATURED -->
|
|
30
30
|
<div v-else-if="layout === 'featured'">
|
|
31
|
-
<div v-if="items[0]" class="p-10
|
|
31
|
+
<div v-if="items[0]" :class="['p-10 border mb-6', cardRound]" :style="{ backgroundColor: textColor + '05', borderColor: textColor + '10' }">
|
|
32
32
|
<div v-if="items[0].rating" class="flex gap-0.5 mb-6">
|
|
33
33
|
<span v-for="s in 5" :key="s" class="text-base" :style="{ opacity: s <= items[0].rating ? 1 : 0.15 }">★</span>
|
|
34
34
|
</div>
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
45
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
46
|
-
<div v-for="(item, i) in items.slice(1)" :key="i" class="p-7
|
|
46
|
+
<div v-for="(item, i) in items.slice(1)" :key="i" :class="['p-7 border', cardRound]" :style="{ backgroundColor: textColor + '03', borderColor: textColor + '08' }">
|
|
47
47
|
<p class="text-sm italic leading-relaxed mb-5" :style="{ opacity: 0.7 }">"{{ item.quote }}"</p>
|
|
48
48
|
<div class="flex items-center gap-3">
|
|
49
49
|
<img v-if="item.avatar" :src="item.avatar" class="w-8 h-8 rounded-full object-cover" />
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
<!-- WALL / MINIMAL -->
|
|
61
61
|
<div v-else class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6">
|
|
62
|
-
<div v-for="(item, i) in items" :key="i" class="break-inside-avoid p-6
|
|
62
|
+
<div v-for="(item, i) in items" :key="i" :class="['break-inside-avoid p-6 border mb-6', cardRound]" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '08' }">
|
|
63
63
|
<div v-if="item.rating" class="flex gap-0.5 mb-3">
|
|
64
64
|
<span v-for="s in 5" :key="s" class="text-[10px]" :style="{ opacity: s <= item.rating ? 1 : 0.15 }">★</span>
|
|
65
65
|
</div>
|
|
@@ -77,7 +77,8 @@
|
|
|
77
77
|
</template>
|
|
78
78
|
|
|
79
79
|
<script setup>
|
|
80
|
-
|
|
80
|
+
import { computed } from "vue";
|
|
81
|
+
const props = defineProps({
|
|
81
82
|
layout: { type: String, default: "grid" },
|
|
82
83
|
title: { type: String, default: "What our clients say" },
|
|
83
84
|
titleTag: { type: String, default: "h2" },
|
|
@@ -85,6 +86,10 @@ defineProps({
|
|
|
85
86
|
description: { type: String, default: "" },
|
|
86
87
|
items: { type: Array, default: () => [] },
|
|
87
88
|
bgColor: { type: String, default: "#ffffff" },
|
|
88
|
-
textColor: { type: String, default: "#000000" }
|
|
89
|
+
textColor: { type: String, default: "#000000" },
|
|
90
|
+
spacing: { type: String, default: "normal" },
|
|
91
|
+
radius: { type: String, default: "rounded" }
|
|
89
92
|
});
|
|
93
|
+
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
94
|
+
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
90
95
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="
|
|
2
|
+
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
3
|
|
|
4
4
|
<!-- SPLIT -->
|
|
5
5
|
<div v-if="layout === 'split'" :class="['max-w-6xl mx-auto flex flex-col md:flex-row gap-12 items-center', reverse ? 'md:flex-row-reverse' : '']">
|
|
@@ -80,10 +80,15 @@ const props = defineProps({
|
|
|
80
80
|
reverse: { type: Boolean, default: false },
|
|
81
81
|
bgColor: { type: String, default: "#ffffff" },
|
|
82
82
|
textColor: { type: String, default: "#000000" },
|
|
83
|
-
ctas: { type: Array, default: () => [] }
|
|
83
|
+
ctas: { type: Array, default: () => [] },
|
|
84
|
+
spacing: { type: String, default: "normal" },
|
|
85
|
+
radius: { type: String, default: "rounded" }
|
|
84
86
|
});
|
|
87
|
+
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
88
|
+
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
85
89
|
const imageContainerClass = computed(() => {
|
|
86
|
-
const
|
|
90
|
+
const r = cardRound.value;
|
|
91
|
+
const styles = { rounded: `aspect-[4/3] ${r}`, square: "aspect-square rounded-xl", circle: "aspect-square rounded-full max-w-[360px] mx-auto", portrait: `aspect-[3/4] ${r} max-w-sm mx-auto` };
|
|
87
92
|
return styles[props.imageStyle] ?? styles.rounded;
|
|
88
93
|
});
|
|
89
94
|
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useAdminBasePath: () => string;
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default section definitions for the visual editor.
|
|
3
|
-
* Keys must match component names in the consuming app's components/sections/ directory.
|
|
4
|
-
* e.g. HeroSection → components/sections/HeroSection.vue → <SectionsHeroSection />
|
|
5
|
-
*
|
|
6
|
-
* Add your own sections via the `extraSections` module option in nuxt.config.ts.
|
|
7
|
-
*/
|
|
8
1
|
export declare const DEFAULT_SECTIONS: Record<string, any>;
|
|
9
2
|
export declare const useAdminSections: () => {
|
|
10
3
|
[x: string]: any;
|
|
@@ -23,6 +23,16 @@ const animationOptions = [
|
|
|
23
23
|
{ value: "typewriter", label: "Typewriter", icon: "\u258C" },
|
|
24
24
|
{ value: "float", label: "Float", icon: "\u301C" }
|
|
25
25
|
];
|
|
26
|
+
const spacingOptions = [
|
|
27
|
+
{ value: "compact", label: "Compact", icon: "\u2194" },
|
|
28
|
+
{ value: "normal", label: "Normal", icon: "\u27F7" },
|
|
29
|
+
{ value: "spacious", label: "Large", icon: "\u2195" }
|
|
30
|
+
];
|
|
31
|
+
const radiusOptions = [
|
|
32
|
+
{ value: "sharp", label: "Sharp", icon: "\u25A1" },
|
|
33
|
+
{ value: "rounded", label: "Rounded", icon: "\u25A2" },
|
|
34
|
+
{ value: "pill", label: "Pill", icon: "\u25EF" }
|
|
35
|
+
];
|
|
26
36
|
const i18n0 = (fr = "") => ({ fr, ar: "", en: "" });
|
|
27
37
|
export const DEFAULT_SECTIONS = {
|
|
28
38
|
HeroSection: {
|
|
@@ -42,7 +52,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
42
52
|
bgColor: "#3d35ff",
|
|
43
53
|
textColor: "#ffffff",
|
|
44
54
|
ctaText: i18n0("Get Started"),
|
|
45
|
-
ctaLink: "/"
|
|
55
|
+
ctaLink: "/",
|
|
56
|
+
spacing: "normal",
|
|
57
|
+
radius: "rounded"
|
|
46
58
|
},
|
|
47
59
|
groups: [
|
|
48
60
|
{ key: "design", label: "Design" },
|
|
@@ -71,6 +83,7 @@ export const DEFAULT_SECTIONS = {
|
|
|
71
83
|
ctaLink: { type: "text", label: "Button URL", group: "design" },
|
|
72
84
|
bgColor: { type: "color", label: "Background", group: "design" },
|
|
73
85
|
textColor: { type: "color", label: "Text Color", group: "design" },
|
|
86
|
+
radius: { type: "cards", label: "Arrondis", group: "design", options: radiusOptions },
|
|
74
87
|
animation: { type: "cards", label: "Animation", group: "animation", options: animationOptions }
|
|
75
88
|
}
|
|
76
89
|
},
|
|
@@ -91,7 +104,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
91
104
|
reverse: false,
|
|
92
105
|
bgColor: "#ffffff",
|
|
93
106
|
textColor: "#000000",
|
|
94
|
-
ctas: []
|
|
107
|
+
ctas: [],
|
|
108
|
+
spacing: "normal",
|
|
109
|
+
radius: "rounded"
|
|
95
110
|
},
|
|
96
111
|
fields: {
|
|
97
112
|
layout: { type: "cards", label: "Layout", options: [
|
|
@@ -121,7 +136,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
121
136
|
link: { type: "text", label: "URL" }
|
|
122
137
|
} },
|
|
123
138
|
bgColor: { type: "color", label: "Background" },
|
|
124
|
-
textColor: { type: "color", label: "Text Color" }
|
|
139
|
+
textColor: { type: "color", label: "Text Color" },
|
|
140
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
141
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
125
142
|
}
|
|
126
143
|
},
|
|
127
144
|
ServiceGrid: {
|
|
@@ -138,7 +155,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
138
155
|
itemTag: "h3",
|
|
139
156
|
items: [],
|
|
140
157
|
bgColor: "#ffffff",
|
|
141
|
-
textColor: "#000000"
|
|
158
|
+
textColor: "#000000",
|
|
159
|
+
spacing: "normal",
|
|
160
|
+
radius: "rounded"
|
|
142
161
|
},
|
|
143
162
|
fields: {
|
|
144
163
|
layout: { type: "cards", label: "Layout", options: [
|
|
@@ -161,7 +180,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
161
180
|
image: { type: "image", label: "Icon/Image" }
|
|
162
181
|
} },
|
|
163
182
|
bgColor: { type: "color", label: "Background" },
|
|
164
|
-
textColor: { type: "color", label: "Text Color" }
|
|
183
|
+
textColor: { type: "color", label: "Text Color" },
|
|
184
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
185
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
165
186
|
}
|
|
166
187
|
},
|
|
167
188
|
Features: {
|
|
@@ -171,7 +192,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
171
192
|
title: i18n0("Why choose us?"),
|
|
172
193
|
items: [],
|
|
173
194
|
bgColor: "#ffffff",
|
|
174
|
-
textColor: "#000000"
|
|
195
|
+
textColor: "#000000",
|
|
196
|
+
spacing: "normal",
|
|
197
|
+
radius: "rounded"
|
|
175
198
|
},
|
|
176
199
|
fields: {
|
|
177
200
|
title: { type: "text", label: "Section Title", i18n: true },
|
|
@@ -180,7 +203,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
180
203
|
description: { type: "textarea", label: "Description", i18n: true }
|
|
181
204
|
} },
|
|
182
205
|
bgColor: { type: "color", label: "Background" },
|
|
183
|
-
textColor: { type: "color", label: "Text Color" }
|
|
206
|
+
textColor: { type: "color", label: "Text Color" },
|
|
207
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
208
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
184
209
|
}
|
|
185
210
|
},
|
|
186
211
|
FAQ: {
|
|
@@ -190,14 +215,18 @@ export const DEFAULT_SECTIONS = {
|
|
|
190
215
|
title: i18n0("FAQ"),
|
|
191
216
|
items: [],
|
|
192
217
|
bgColor: "#F9F9FB",
|
|
193
|
-
textColor: "#000000"
|
|
218
|
+
textColor: "#000000",
|
|
219
|
+
spacing: "normal",
|
|
220
|
+
radius: "rounded"
|
|
194
221
|
},
|
|
195
222
|
fields: {
|
|
196
223
|
title: { type: "text", label: "Title", i18n: true },
|
|
197
224
|
items: { type: "list", label: "Questions", itemFields: {
|
|
198
225
|
question: { type: "text", label: "Question", i18n: true },
|
|
199
226
|
answer: { type: "textarea", label: "Answer", i18n: true }
|
|
200
|
-
} }
|
|
227
|
+
} },
|
|
228
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
229
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
201
230
|
}
|
|
202
231
|
},
|
|
203
232
|
Process: {
|
|
@@ -211,7 +240,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
211
240
|
description: i18n0(),
|
|
212
241
|
items: [],
|
|
213
242
|
bgColor: "#ffffff",
|
|
214
|
-
textColor: "#000000"
|
|
243
|
+
textColor: "#000000",
|
|
244
|
+
spacing: "normal",
|
|
245
|
+
radius: "rounded"
|
|
215
246
|
},
|
|
216
247
|
fields: {
|
|
217
248
|
layout: { type: "cards", label: "Layout", options: [
|
|
@@ -231,7 +262,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
231
262
|
description: { type: "textarea", label: "Description", i18n: true }
|
|
232
263
|
} },
|
|
233
264
|
bgColor: { type: "color", label: "Background" },
|
|
234
|
-
textColor: { type: "color", label: "Text Color" }
|
|
265
|
+
textColor: { type: "color", label: "Text Color" },
|
|
266
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
267
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
235
268
|
}
|
|
236
269
|
},
|
|
237
270
|
Testimonials: {
|
|
@@ -245,7 +278,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
245
278
|
description: i18n0(),
|
|
246
279
|
items: [],
|
|
247
280
|
bgColor: "#ffffff",
|
|
248
|
-
textColor: "#000000"
|
|
281
|
+
textColor: "#000000",
|
|
282
|
+
spacing: "normal",
|
|
283
|
+
radius: "rounded"
|
|
249
284
|
},
|
|
250
285
|
fields: {
|
|
251
286
|
layout: { type: "cards", label: "Layout", options: [
|
|
@@ -267,7 +302,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
267
302
|
avatar: { type: "image", label: "Photo" }
|
|
268
303
|
} },
|
|
269
304
|
bgColor: { type: "color", label: "Background" },
|
|
270
|
-
textColor: { type: "color", label: "Text Color" }
|
|
305
|
+
textColor: { type: "color", label: "Text Color" },
|
|
306
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
307
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
271
308
|
}
|
|
272
309
|
},
|
|
273
310
|
Pricing: {
|
|
@@ -281,7 +318,9 @@ export const DEFAULT_SECTIONS = {
|
|
|
281
318
|
description: i18n0(),
|
|
282
319
|
items: [],
|
|
283
320
|
bgColor: "#ffffff",
|
|
284
|
-
textColor: "#000000"
|
|
321
|
+
textColor: "#000000",
|
|
322
|
+
spacing: "normal",
|
|
323
|
+
radius: "rounded"
|
|
285
324
|
},
|
|
286
325
|
fields: {
|
|
287
326
|
layout: { type: "cards", label: "Layout", options: [
|
|
@@ -303,27 +342,39 @@ export const DEFAULT_SECTIONS = {
|
|
|
303
342
|
highlighted: { type: "select", label: "Highlight", options: [{ value: false, label: "No" }, { value: true, label: "Yes" }] }
|
|
304
343
|
} },
|
|
305
344
|
bgColor: { type: "color", label: "Background" },
|
|
306
|
-
textColor: { type: "color", label: "Text Color" }
|
|
345
|
+
textColor: { type: "color", label: "Text Color" },
|
|
346
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
347
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
307
348
|
}
|
|
308
349
|
},
|
|
309
350
|
ContactForm: {
|
|
310
351
|
label: "Contact Form",
|
|
311
352
|
icon: "\u{1F4E9}",
|
|
312
353
|
defaultProps: {
|
|
354
|
+
layout: "split",
|
|
313
355
|
title: i18n0("Let's talk"),
|
|
314
356
|
subtitle: i18n0("Describe your project and we'll get back to you within 24h."),
|
|
315
357
|
badge: i18n0(),
|
|
316
358
|
button_text: i18n0("Send"),
|
|
317
359
|
bgColor: "#ffffff",
|
|
318
|
-
textColor: "#000000"
|
|
360
|
+
textColor: "#000000",
|
|
361
|
+
spacing: "normal",
|
|
362
|
+
radius: "rounded"
|
|
319
363
|
},
|
|
320
364
|
fields: {
|
|
365
|
+
layout: { type: "cards", label: "Layout", options: [
|
|
366
|
+
{ value: "split", label: "Split", icon: "\u25E7" },
|
|
367
|
+
{ value: "centered", label: "Centered", icon: "\u25C9" },
|
|
368
|
+
{ value: "minimal", label: "Minimal", icon: "\u2014" }
|
|
369
|
+
] },
|
|
321
370
|
badge: { type: "text", label: "Badge", i18n: true },
|
|
322
371
|
title: { type: "text", label: "Title", i18n: true },
|
|
323
372
|
subtitle: { type: "text", label: "Subtitle", i18n: true },
|
|
324
373
|
button_text: { type: "text", label: "Button", i18n: true },
|
|
325
374
|
bgColor: { type: "color", label: "Background" },
|
|
326
|
-
textColor: { type: "color", label: "Text Color" }
|
|
375
|
+
textColor: { type: "color", label: "Text Color" },
|
|
376
|
+
spacing: { type: "cards", label: "Espacement", options: spacingOptions },
|
|
377
|
+
radius: { type: "cards", label: "Arrondis", options: radiusOptions }
|
|
327
378
|
}
|
|
328
379
|
}
|
|
329
380
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface SectionPreset {
|
|
2
|
+
layout?: string;
|
|
3
|
+
spacing?: string;
|
|
4
|
+
radius?: string;
|
|
5
|
+
animation?: string;
|
|
6
|
+
bgColor?: string;
|
|
7
|
+
textColor?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BrandSettings {
|
|
10
|
+
id: number;
|
|
11
|
+
brand_name?: string;
|
|
12
|
+
tagline?: string;
|
|
13
|
+
vision?: string;
|
|
14
|
+
logo_url?: string;
|
|
15
|
+
primary_color?: string;
|
|
16
|
+
secondary_color?: string;
|
|
17
|
+
accent_color?: string;
|
|
18
|
+
font_headline?: string;
|
|
19
|
+
font_body?: string;
|
|
20
|
+
ai_context?: string;
|
|
21
|
+
section_presets?: Record<string, SectionPreset>;
|
|
22
|
+
updated_at?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const useAdminSettings: () => {
|
|
25
|
+
settings: any;
|
|
26
|
+
loading: any;
|
|
27
|
+
saving: any;
|
|
28
|
+
load: () => Promise<void>;
|
|
29
|
+
save: (updates: Partial<BrandSettings>) => Promise<void>;
|
|
30
|
+
applyPresets: (sectionType: string, defaultProps: Record<string, any>) => Record<string, any>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ref } from "#imports";
|
|
2
|
+
import { useSupabaseClient } from "#imports";
|
|
3
|
+
export const useAdminSettings = () => {
|
|
4
|
+
const supabase = useSupabaseClient();
|
|
5
|
+
const settings = ref(null);
|
|
6
|
+
const loading = ref(false);
|
|
7
|
+
const saving = ref(false);
|
|
8
|
+
const load = async () => {
|
|
9
|
+
loading.value = true;
|
|
10
|
+
const { data } = await supabase.from("brand_settings").select("*").eq("id", 1).maybeSingle();
|
|
11
|
+
settings.value = data || { id: 1 };
|
|
12
|
+
loading.value = false;
|
|
13
|
+
};
|
|
14
|
+
const save = async (updates) => {
|
|
15
|
+
saving.value = true;
|
|
16
|
+
await supabase.from("brand_settings").upsert({ id: 1, ...updates });
|
|
17
|
+
settings.value = { ...settings.value || { id: 1 }, ...updates };
|
|
18
|
+
saving.value = false;
|
|
19
|
+
};
|
|
20
|
+
const applyPresets = (sectionType, defaultProps) => {
|
|
21
|
+
const presets = settings.value?.section_presets?.[sectionType];
|
|
22
|
+
if (!presets) return defaultProps;
|
|
23
|
+
const merged = { ...defaultProps };
|
|
24
|
+
const styleKeys = ["layout", "spacing", "radius", "animation", "bgColor", "textColor"];
|
|
25
|
+
for (const key of styleKeys) {
|
|
26
|
+
if (presets[key] !== void 0) merged[key] = presets[key];
|
|
27
|
+
}
|
|
28
|
+
return merged;
|
|
29
|
+
};
|
|
30
|
+
return { settings, loading, saving, load, save, applyPresets };
|
|
31
|
+
};
|
|
@@ -4,10 +4,11 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
4
4
|
const config = useRuntimeConfig().public.adminCms;
|
|
5
5
|
const user = useSupabaseUser();
|
|
6
6
|
const loginRoute = config?.loginRoute || "/login";
|
|
7
|
-
|
|
7
|
+
const basePath = config?.basePath || "/admin";
|
|
8
|
+
if (to.path.startsWith(basePath) && !user.value) {
|
|
8
9
|
return navigateTo(loginRoute);
|
|
9
10
|
}
|
|
10
11
|
if (to.path === loginRoute && user.value) {
|
|
11
|
-
return navigateTo(
|
|
12
|
+
return navigateTo(basePath);
|
|
12
13
|
}
|
|
13
14
|
});
|