cinqcinqdev-seo 0.1.8 → 0.1.10
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.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -11
- package/dist/runtime/assets/admin-tw.css +1 -1
- package/dist/runtime/components/admin/ContentCalendar.d.vue.ts +3 -0
- package/dist/runtime/components/admin/ContentCalendar.vue +196 -0
- package/dist/runtime/components/admin/ContentCalendar.vue.d.ts +3 -0
- package/dist/runtime/components/admin/DashboardLayout.d.vue.ts +13 -0
- package/dist/runtime/components/admin/DashboardLayout.vue +12 -0
- package/dist/runtime/components/admin/DashboardLayout.vue.d.ts +13 -0
- package/dist/runtime/components/admin/NavItem.d.vue.ts +3 -0
- package/dist/runtime/components/admin/NavItem.vue +46 -0
- package/dist/runtime/components/admin/NavItem.vue.d.ts +3 -0
- package/dist/runtime/components/admin/Navbar.vue +1 -1
- package/dist/runtime/components/admin/Sidebar.d.vue.ts +3 -0
- package/dist/runtime/components/admin/Sidebar.vue +115 -0
- package/dist/runtime/components/admin/Sidebar.vue.d.ts +3 -0
- package/dist/runtime/composables/useAdminBranding.d.ts +4 -0
- package/dist/runtime/composables/useAdminBranding.js +22 -0
- package/dist/runtime/composables/useAdminContent.d.ts +9 -0
- package/dist/runtime/composables/useAdminContent.js +46 -0
- package/dist/runtime/composables/useAdminPersonas.d.ts +8 -0
- package/dist/runtime/composables/useAdminPersonas.js +31 -0
- package/dist/runtime/composables/useAdminProducts.d.ts +6 -0
- package/dist/runtime/composables/useAdminProducts.js +24 -0
- package/dist/runtime/composables/useAdminSections.js +1 -0
- package/dist/runtime/composables/useAdminStrategies.d.ts +10 -0
- package/dist/runtime/composables/useAdminStrategies.js +57 -0
- package/dist/runtime/composables/useAdminTodos.d.ts +10 -0
- package/dist/runtime/composables/useAdminTodos.js +55 -0
- package/dist/runtime/middleware/admin-auth.js +2 -0
- package/dist/runtime/pages/admin/account.vue +1 -0
- package/dist/runtime/pages/admin/branding.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/branding.vue +276 -0
- package/dist/runtime/pages/admin/branding.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/calendar.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/calendar.vue +169 -0
- package/dist/runtime/pages/admin/calendar.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/content-library.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/content-library.vue +359 -0
- package/dist/runtime/pages/admin/content-library.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/editor/[id].vue +5 -2
- package/dist/runtime/pages/admin/index.vue +81 -22
- package/dist/runtime/pages/admin/pages/[type].vue +1 -0
- package/dist/runtime/pages/admin/personas/[id].d.vue.ts +3 -0
- package/dist/runtime/pages/admin/personas/[id].vue +170 -0
- package/dist/runtime/pages/admin/personas/[id].vue.d.ts +3 -0
- package/dist/runtime/pages/admin/personas/index.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/personas/index.vue +266 -0
- package/dist/runtime/pages/admin/personas/index.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/products.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/products.vue +300 -0
- package/dist/runtime/pages/admin/products.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/strategies/[id].d.vue.ts +3 -0
- package/dist/runtime/pages/admin/strategies/[id].vue +237 -0
- package/dist/runtime/pages/admin/strategies/[id].vue.d.ts +3 -0
- package/dist/runtime/pages/admin/strategies/index.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/strategies/index.vue +232 -0
- package/dist/runtime/pages/admin/strategies/index.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/todos.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/todos.vue +251 -0
- package/dist/runtime/pages/admin/todos.vue.d.ts +3 -0
- package/dist/runtime/plugins/admin-font.client.d.ts +0 -5
- package/dist/runtime/plugins/admin-font.client.js +1 -0
- package/dist/runtime/stores/adminUser.d.ts +29 -1
- package/dist/runtime/stores/adminUser.js +4 -2
- package/dist/types.d.mts +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onMounted, useHead } from "#imports";
|
|
3
|
+
import { useAdminContent, useAdminPersonas } from "#imports";
|
|
4
|
+
useHead({ title: "Content Library - Admin" });
|
|
5
|
+
const { fetchContent, createContent, updateContent, deleteContent, publishContent } = useAdminContent();
|
|
6
|
+
const { fetchPersonas } = useAdminPersonas();
|
|
7
|
+
const allContents = ref([]);
|
|
8
|
+
const personas = ref([]);
|
|
9
|
+
const loading = ref(true);
|
|
10
|
+
const showModal = ref(false);
|
|
11
|
+
const saving = ref(false);
|
|
12
|
+
const editingContent = ref(null);
|
|
13
|
+
const viewMode = ref("columns");
|
|
14
|
+
const activeTab = ref("Tous");
|
|
15
|
+
const activePlatform = ref("Tous");
|
|
16
|
+
const tabs = ["Tous", "Revision", "Planifies", "Publies", "Archives"];
|
|
17
|
+
const platforms = ["LinkedIn", "Instagram", "Facebook", "Twitter", "Email", "Blog", "Website"];
|
|
18
|
+
const allPlatforms = ["Tous", ...platforms];
|
|
19
|
+
const instagramTypes = ["Story", "Reel", "Carousel", "Post"];
|
|
20
|
+
const defaultForm = () => ({
|
|
21
|
+
title: "",
|
|
22
|
+
platform: "LinkedIn",
|
|
23
|
+
content_type: "Post Social",
|
|
24
|
+
content: "",
|
|
25
|
+
persona_id: "",
|
|
26
|
+
persona: "",
|
|
27
|
+
status: "draft",
|
|
28
|
+
scheduled_date: "",
|
|
29
|
+
instagram_type: "",
|
|
30
|
+
content_url: ""
|
|
31
|
+
});
|
|
32
|
+
const formData = ref(defaultForm());
|
|
33
|
+
onMounted(async () => {
|
|
34
|
+
loading.value = true;
|
|
35
|
+
try {
|
|
36
|
+
const [c, p] = await Promise.all([fetchContent(), fetchPersonas()]);
|
|
37
|
+
allContents.value = c || [];
|
|
38
|
+
personas.value = p || [];
|
|
39
|
+
} catch {
|
|
40
|
+
} finally {
|
|
41
|
+
loading.value = false;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const getPlatformConfig = (platform) => {
|
|
45
|
+
const map = {
|
|
46
|
+
LinkedIn: { icon: "mdi:linkedin", bg: "bg-blue-50", color: "text-blue-600" },
|
|
47
|
+
Instagram: { icon: "mdi:instagram", bg: "bg-pink-50", color: "text-pink-600" },
|
|
48
|
+
Facebook: { icon: "mdi:facebook", bg: "bg-blue-50", color: "text-blue-600" },
|
|
49
|
+
Twitter: { icon: "mdi:twitter", bg: "bg-sky-50", color: "text-sky-600" },
|
|
50
|
+
Email: { icon: "mdi:email-outline", bg: "bg-purple-50", color: "text-purple-600" },
|
|
51
|
+
Blog: { icon: "mdi:newspaper-variant-outline", bg: "bg-emerald-50", color: "text-emerald-600" },
|
|
52
|
+
Website: { icon: "mdi:web", bg: "bg-blue-50", color: "text-blue-600" }
|
|
53
|
+
};
|
|
54
|
+
return map[platform] || { icon: "mdi:dots-horizontal-circle", bg: "bg-slate-50", color: "text-slate-600" };
|
|
55
|
+
};
|
|
56
|
+
const enrichContent = (c) => {
|
|
57
|
+
const pc = getPlatformConfig(c.platform);
|
|
58
|
+
const statusMap = {
|
|
59
|
+
draft: "bg-slate-100 text-slate-500",
|
|
60
|
+
review: "bg-orange-100 text-orange-600",
|
|
61
|
+
approved: "bg-blue-100 text-blue-700",
|
|
62
|
+
published: "bg-emerald-100 text-emerald-700",
|
|
63
|
+
archived: "bg-gray-100 text-gray-500"
|
|
64
|
+
};
|
|
65
|
+
const labelMap = { draft: "Idee", review: "Revision", approved: "Planifie", published: "Publie", archived: "Archive" };
|
|
66
|
+
return {
|
|
67
|
+
...c,
|
|
68
|
+
platformIcon: pc.icon,
|
|
69
|
+
platformBg: pc.bg,
|
|
70
|
+
platformColor: pc.color,
|
|
71
|
+
statusClass: statusMap[c.status] || statusMap.draft,
|
|
72
|
+
statusLabel: labelMap[c.status] || c.status,
|
|
73
|
+
preview: c.content?.substring(0, 150) || "",
|
|
74
|
+
persona: c.persona || "Generique",
|
|
75
|
+
date: c.created_at ? new Date(c.created_at).toLocaleDateString("fr-FR") : "",
|
|
76
|
+
wordCount: c.content ? c.content.split(" ").length : 0
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const contents = computed(() => {
|
|
80
|
+
let filtered = allContents.value;
|
|
81
|
+
if (activeTab.value !== "Tous") {
|
|
82
|
+
const sm = { Revision: "review", Planifies: "approved", Publies: "published", Archives: "archived" };
|
|
83
|
+
filtered = filtered.filter((c) => c.status === sm[activeTab.value]);
|
|
84
|
+
}
|
|
85
|
+
if (activePlatform.value !== "Tous") filtered = filtered.filter((c) => c.platform === activePlatform.value);
|
|
86
|
+
return filtered.map(enrichContent);
|
|
87
|
+
});
|
|
88
|
+
const stats = computed(() => [
|
|
89
|
+
{ label: "Total", value: allContents.value.length, icon: "mdi:archive-outline", bg: "bg-slate-100", color: "text-slate-900" },
|
|
90
|
+
{ label: "Publies", value: allContents.value.filter((c) => c.status === "published").length, icon: "mdi:check-decagram", bg: "bg-emerald-50", color: "text-emerald-500" },
|
|
91
|
+
{ label: "Planifies", value: allContents.value.filter((c) => c.status === "approved").length, icon: "mdi:calendar-check", bg: "bg-blue-50", color: "text-blue-500" },
|
|
92
|
+
{ label: "Idees", value: allContents.value.filter((c) => c.status === "draft").length, icon: "mdi:lightbulb-outline", bg: "bg-amber-50", color: "text-amber-500" },
|
|
93
|
+
{ label: "Revision", value: allContents.value.filter((c) => c.status === "review").length, icon: "mdi:pencil-ruler", bg: "bg-orange-50", color: "text-orange-500" }
|
|
94
|
+
]);
|
|
95
|
+
const getContentByPlatform = (platform) => contents.value.filter((c) => c.platform === platform);
|
|
96
|
+
const updatePersonaName = () => {
|
|
97
|
+
const p = personas.value.find((p2) => p2.id === formData.value.persona_id);
|
|
98
|
+
formData.value.persona = p ? p.name : "";
|
|
99
|
+
};
|
|
100
|
+
const openCreate = (platform) => {
|
|
101
|
+
editingContent.value = null;
|
|
102
|
+
formData.value = { ...defaultForm(), platform: platform || "LinkedIn" };
|
|
103
|
+
showModal.value = true;
|
|
104
|
+
};
|
|
105
|
+
const openEdit = (content) => {
|
|
106
|
+
editingContent.value = content;
|
|
107
|
+
formData.value = {
|
|
108
|
+
...content,
|
|
109
|
+
scheduled_date: content.scheduled_date ? new Date(content.scheduled_date).toISOString().slice(0, 16) : ""
|
|
110
|
+
};
|
|
111
|
+
showModal.value = true;
|
|
112
|
+
};
|
|
113
|
+
const closeModal = () => {
|
|
114
|
+
showModal.value = false;
|
|
115
|
+
editingContent.value = null;
|
|
116
|
+
};
|
|
117
|
+
const saveContent = async () => {
|
|
118
|
+
if (!formData.value.title || !formData.value.content) return alert("Titre et contenu requis");
|
|
119
|
+
saving.value = true;
|
|
120
|
+
try {
|
|
121
|
+
const data = { ...formData.value, persona_id: formData.value.persona_id || null };
|
|
122
|
+
if (data.scheduled_date) data.scheduled_date = new Date(data.scheduled_date).toISOString();
|
|
123
|
+
editingContent.value ? await updateContent(editingContent.value.id, data) : await createContent(data);
|
|
124
|
+
allContents.value = await fetchContent();
|
|
125
|
+
closeModal();
|
|
126
|
+
} catch (e) {
|
|
127
|
+
alert("Erreur: " + (e.message || "Inconnue"));
|
|
128
|
+
} finally {
|
|
129
|
+
saving.value = false;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const handlePublish = async (content) => {
|
|
133
|
+
if (confirm("Publier ce contenu ?")) {
|
|
134
|
+
await publishContent(content.id);
|
|
135
|
+
allContents.value = await fetchContent();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const handleDelete = async (content) => {
|
|
139
|
+
if (confirm("Supprimer ?")) {
|
|
140
|
+
await deleteContent(content.id);
|
|
141
|
+
allContents.value = await fetchContent();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<template>
|
|
147
|
+
<AdminDashboardLayout>
|
|
148
|
+
<div class="max-w-7xl mx-auto px-6 md:px-10 py-10">
|
|
149
|
+
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
150
|
+
<div>
|
|
151
|
+
<h1 class="text-4xl font-black text-slate-900">Content Library</h1>
|
|
152
|
+
<p class="text-slate-500 mt-1">Gerez votre arsenal de contenus</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="flex items-center gap-3 bg-white p-2 rounded-2xl border border-slate-200 shadow-sm">
|
|
155
|
+
<button @click="viewMode = 'columns'" :class="[viewMode === 'columns' ? 'bg-emerald-600 text-white shadow-lg' : 'text-slate-500 hover:bg-slate-50']" class="px-5 py-2.5 rounded-xl text-xs font-black uppercase transition-all flex items-center gap-2">
|
|
156
|
+
<Icon name="mdi:view-column" /> Colonnes
|
|
157
|
+
</button>
|
|
158
|
+
<button @click="viewMode = 'table'" :class="[viewMode === 'table' ? 'bg-slate-900 text-white shadow-lg' : 'text-slate-500 hover:bg-slate-50']" class="px-5 py-2.5 rounded-xl text-xs font-black uppercase transition-all flex items-center gap-2">
|
|
159
|
+
<Icon name="mdi:table" /> Tableau
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
|
165
|
+
<div v-for="stat in stats" :key="stat.label" class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm">
|
|
166
|
+
<div :class="[stat.bg, 'w-10 h-10 rounded-xl flex items-center justify-center mb-3']">
|
|
167
|
+
<Icon :name="stat.icon" :class="[stat.color, 'text-xl']" />
|
|
168
|
+
</div>
|
|
169
|
+
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ stat.label }}</p>
|
|
170
|
+
<p class="text-2xl font-black text-slate-900">{{ stat.value }}</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<!-- Columns View -->
|
|
175
|
+
<div v-if="viewMode === 'columns'" class="overflow-x-auto pb-6">
|
|
176
|
+
<div class="flex gap-6 min-w-max">
|
|
177
|
+
<div v-for="platform in platforms" :key="platform" class="flex-shrink-0 w-80">
|
|
178
|
+
<div class="bg-white rounded-2xl border border-slate-200 overflow-hidden shadow-sm">
|
|
179
|
+
<div class="p-5 border-b border-slate-100 bg-slate-50">
|
|
180
|
+
<div class="flex items-center justify-between mb-2">
|
|
181
|
+
<div class="flex items-center gap-3">
|
|
182
|
+
<div :class="[getPlatformConfig(platform).bg, 'w-10 h-10 rounded-xl flex items-center justify-center']">
|
|
183
|
+
<Icon :name="getPlatformConfig(platform).icon" :class="[getPlatformConfig(platform).color, 'text-xl']" />
|
|
184
|
+
</div>
|
|
185
|
+
<h3 class="font-black text-slate-900">{{ platform }}</h3>
|
|
186
|
+
</div>
|
|
187
|
+
<span class="px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-xs font-bold">{{ getContentByPlatform(platform).length }}</span>
|
|
188
|
+
</div>
|
|
189
|
+
<button @click="openCreate(platform)" class="w-full mt-2 py-2 bg-blue-50 hover:bg-blue-100 text-blue-600 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2">
|
|
190
|
+
<Icon name="mdi:plus" /> Ajouter
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="p-4 space-y-3 max-h-96 overflow-y-auto">
|
|
194
|
+
<div v-for="content in getContentByPlatform(platform)" :key="content.id"
|
|
195
|
+
class="bg-white border-2 border-slate-100 rounded-xl p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer group"
|
|
196
|
+
@click="openEdit(content)">
|
|
197
|
+
<span :class="['px-2 py-1 rounded-lg text-[9px] font-black uppercase', content.statusClass]">{{ content.statusLabel }}</span>
|
|
198
|
+
<p class="font-bold text-slate-900 text-sm mt-2 line-clamp-2 group-hover:text-blue-600 transition-colors">{{ content.title }}</p>
|
|
199
|
+
<p class="text-xs text-slate-500 line-clamp-2 mt-1">{{ content.preview }}</p>
|
|
200
|
+
<div class="flex items-center justify-between text-[10px] text-slate-400 mt-2">
|
|
201
|
+
<span>{{ content.persona }}</span>
|
|
202
|
+
<span>{{ content.wordCount }} mots</span>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<div v-if="getContentByPlatform(platform).length === 0" class="text-center py-10 text-slate-300">
|
|
206
|
+
<Icon name="mdi:inbox" class="text-4xl mb-2" />
|
|
207
|
+
<p class="text-xs">Aucun contenu</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<!-- Table View -->
|
|
216
|
+
<div v-else class="space-y-4">
|
|
217
|
+
<div class="bg-white rounded-2xl p-4 border border-slate-200 shadow-sm space-y-4">
|
|
218
|
+
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
219
|
+
<div class="flex gap-2">
|
|
220
|
+
<button v-for="tab in tabs" :key="tab" @click="activeTab = tab"
|
|
221
|
+
:class="[activeTab === tab ? 'bg-slate-900 text-white' : 'text-slate-500 hover:bg-slate-50']"
|
|
222
|
+
class="px-4 py-2 rounded-xl text-xs font-black uppercase transition-all">{{ tab }}</button>
|
|
223
|
+
</div>
|
|
224
|
+
<button @click="openCreate()" class="bg-blue-600 text-white px-5 py-2 rounded-xl text-xs font-black uppercase hover:bg-blue-700 transition-all flex items-center gap-2">
|
|
225
|
+
<Icon name="mdi:plus" /> Nouveau
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="flex gap-2 flex-wrap pt-2 border-t border-slate-100">
|
|
229
|
+
<button v-for="p in allPlatforms" :key="p" @click="activePlatform = p"
|
|
230
|
+
:class="[activePlatform === p ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200']"
|
|
231
|
+
class="px-3 py-1.5 rounded-xl text-xs font-bold transition-all">{{ p }}</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div class="bg-white rounded-2xl border border-slate-200 overflow-hidden shadow-sm">
|
|
236
|
+
<table class="w-full text-left">
|
|
237
|
+
<thead class="bg-slate-50 border-b border-slate-200">
|
|
238
|
+
<tr>
|
|
239
|
+
<th class="px-6 py-4 text-[10px] font-black uppercase text-slate-400 tracking-widest">Contenu / Plateforme</th>
|
|
240
|
+
<th class="px-6 py-4 text-[10px] font-black uppercase text-slate-400 tracking-widest">Statut</th>
|
|
241
|
+
<th class="px-6 py-4 text-[10px] font-black uppercase text-slate-400 tracking-widest">Persona</th>
|
|
242
|
+
<th class="px-6 py-4 text-[10px] font-black uppercase text-slate-400 tracking-widest">Actions</th>
|
|
243
|
+
</tr>
|
|
244
|
+
</thead>
|
|
245
|
+
<tbody class="divide-y divide-slate-100">
|
|
246
|
+
<tr v-for="content in contents" :key="content.id" class="hover:bg-slate-50/50 transition-colors group">
|
|
247
|
+
<td class="px-6 py-4">
|
|
248
|
+
<div class="flex items-center gap-4">
|
|
249
|
+
<div :class="[content.platformBg, 'w-10 h-10 rounded-xl flex items-center justify-center']">
|
|
250
|
+
<Icon :name="content.platformIcon" :class="[content.platformColor, 'text-xl']" />
|
|
251
|
+
</div>
|
|
252
|
+
<div>
|
|
253
|
+
<p class="font-bold text-slate-900">{{ content.title }}</p>
|
|
254
|
+
<p class="text-[10px] text-slate-400 uppercase">{{ content.date }} • {{ content.wordCount }} mots</p>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</td>
|
|
258
|
+
<td class="px-6 py-4"><span :class="['px-3 py-1 rounded-lg text-[10px] font-black uppercase', content.statusClass]">{{ content.statusLabel }}</span></td>
|
|
259
|
+
<td class="px-6 py-4"><span class="text-xs font-bold text-slate-600 italic">{{ content.persona }}</span></td>
|
|
260
|
+
<td class="px-6 py-4">
|
|
261
|
+
<div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
262
|
+
<button @click="openEdit(content)" class="p-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-600 hover:text-white transition-all"><Icon name="mdi:pencil" /></button>
|
|
263
|
+
<button v-if="content.status === 'draft'" @click="handlePublish(content)" class="p-2 bg-emerald-50 text-emerald-600 rounded-lg hover:bg-emerald-600 hover:text-white transition-all"><Icon name="mdi:send" /></button>
|
|
264
|
+
<button @click="handleDelete(content)" class="p-2 bg-red-50 text-red-600 rounded-lg hover:bg-red-600 hover:text-white transition-all"><Icon name="mdi:delete" /></button>
|
|
265
|
+
</div>
|
|
266
|
+
</td>
|
|
267
|
+
</tr>
|
|
268
|
+
</tbody>
|
|
269
|
+
</table>
|
|
270
|
+
<div v-if="contents.length === 0" class="text-center py-12 text-slate-300">
|
|
271
|
+
<Icon name="mdi:inbox" class="text-5xl mb-2" />
|
|
272
|
+
<p class="text-sm">Aucun contenu</p>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- Modal -->
|
|
279
|
+
<Teleport to="body">
|
|
280
|
+
<div v-if="showModal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-lg z-50 flex items-center justify-center p-4">
|
|
281
|
+
<div class="bg-white rounded-[2rem] max-w-4xl w-full max-h-[95vh] overflow-hidden shadow-2xl flex flex-col">
|
|
282
|
+
<div class="bg-gradient-to-r from-slate-900 to-slate-800 p-8 text-white flex justify-between items-center">
|
|
283
|
+
<h2 class="text-2xl font-black">{{ editingContent ? "Editer le Contenu" : "Nouveau Contenu" }}</h2>
|
|
284
|
+
<button @click="closeModal" class="w-12 h-12 bg-white/10 hover:bg-red-500 rounded-2xl flex items-center justify-center transition-all">
|
|
285
|
+
<Icon name="mdi:close" class="text-2xl" />
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="p-8 overflow-y-auto space-y-6">
|
|
289
|
+
<div>
|
|
290
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">Titre *</label>
|
|
291
|
+
<input v-model="formData.title" class="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-5 py-4 font-bold text-slate-900" placeholder="Titre du contenu" />
|
|
292
|
+
</div>
|
|
293
|
+
<div class="grid grid-cols-2 gap-4">
|
|
294
|
+
<div>
|
|
295
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">Plateforme</label>
|
|
296
|
+
<div class="grid grid-cols-3 gap-2">
|
|
297
|
+
<button v-for="p in platforms" :key="p" @click="formData.platform = p"
|
|
298
|
+
:class="[formData.platform === p ? 'bg-blue-600 text-white scale-105' : 'bg-white border-2 border-slate-200 text-slate-600 hover:border-blue-300']"
|
|
299
|
+
class="px-3 py-2 rounded-xl text-xs font-black uppercase transition-all">{{ p }}</button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
<div v-if="formData.platform === 'Instagram'">
|
|
303
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">Type Instagram</label>
|
|
304
|
+
<div class="grid grid-cols-2 gap-2">
|
|
305
|
+
<button v-for="t in instagramTypes" :key="t" @click="formData.instagram_type = t"
|
|
306
|
+
:class="[formData.instagram_type === t ? 'bg-pink-600 text-white' : 'bg-white border-2 border-slate-200 text-pink-600 hover:border-pink-300']"
|
|
307
|
+
class="px-3 py-2 rounded-xl text-xs font-black uppercase transition-all">{{ t }}</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
<div>
|
|
312
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">Contenu *</label>
|
|
313
|
+
<textarea v-model="formData.content" rows="8" class="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-5 py-4 font-medium text-slate-700 resize-none" placeholder="Corps du message..."></textarea>
|
|
314
|
+
<div class="mt-1 flex justify-between text-xs text-slate-400">
|
|
315
|
+
<span>{{ formData.content.length }} car.</span>
|
|
316
|
+
<span>{{ formData.content.split(" ").filter((w) => w).length }} mots</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="grid grid-cols-2 gap-4">
|
|
320
|
+
<div>
|
|
321
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">Persona</label>
|
|
322
|
+
<select v-model="formData.persona_id" @change="updatePersonaName" class="w-full bg-white border-2 border-slate-200 rounded-xl px-4 py-3 font-bold text-slate-700">
|
|
323
|
+
<option value="">Generique</option>
|
|
324
|
+
<option v-for="p in personas" :key="p.id" :value="p.id">{{ p.name }}</option>
|
|
325
|
+
</select>
|
|
326
|
+
</div>
|
|
327
|
+
<div>
|
|
328
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">Statut</label>
|
|
329
|
+
<div class="grid grid-cols-2 gap-1.5">
|
|
330
|
+
<button v-for="s in [{ v: 'draft', l: 'Idee' }, { v: 'review', l: 'Revision' }, { v: 'approved', l: 'Planifie' }, { v: 'published', l: 'Publie' }, { v: 'archived', l: 'Archive' }]" :key="s.v"
|
|
331
|
+
@click="formData.status = s.v"
|
|
332
|
+
:class="[formData.status === s.v ? 'bg-blue-600 text-white scale-105' : 'bg-white border-2 border-slate-200 text-slate-600 hover:border-blue-300']"
|
|
333
|
+
class="px-3 py-2 rounded-xl text-[10px] font-black uppercase transition-all">{{ s.l }}</button>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="grid grid-cols-2 gap-4">
|
|
338
|
+
<div>
|
|
339
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">Date prevue</label>
|
|
340
|
+
<input v-model="formData.scheduled_date" type="datetime-local" class="w-full bg-white border-2 border-indigo-200 rounded-xl px-4 py-3 font-bold text-indigo-900" />
|
|
341
|
+
</div>
|
|
342
|
+
<div>
|
|
343
|
+
<label class="text-xs font-black uppercase text-slate-500 mb-2 block">URL publie</label>
|
|
344
|
+
<input v-model="formData.content_url" type="url" placeholder="https://..." class="w-full bg-white border-2 border-cyan-200 rounded-xl px-4 py-3 font-bold text-cyan-900" />
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="p-6 bg-slate-50 border-t-2 border-slate-200 flex justify-end gap-3">
|
|
349
|
+
<button @click="closeModal" class="px-8 py-3 rounded-xl font-black text-sm uppercase text-slate-600 bg-white border-2 border-slate-200 hover:bg-slate-100 transition-all">Annuler</button>
|
|
350
|
+
<button @click="saveContent" :disabled="saving" class="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-12 py-3 rounded-xl font-black text-sm uppercase shadow-xl disabled:opacity-50 flex items-center gap-2">
|
|
351
|
+
<Icon :name="saving ? 'mdi:loading' : 'mdi:content-save'" :class="{ 'animate-spin': saving }" />
|
|
352
|
+
{{ saving ? "Enregistrement..." : editingContent ? "Mettre a Jour" : "Creer" }}
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</Teleport>
|
|
358
|
+
</AdminDashboardLayout>
|
|
359
|
+
</template>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { ref, computed, watch, definePageMeta, useRoute, useRuntimeConfig, useAsyncData, useHead, useSupabaseClient, useNuxtApp } from "#imports";
|
|
2
3
|
import { useAdminSections } from "../../../composables/useAdminSections";
|
|
3
4
|
definePageMeta({ layout: false });
|
|
4
5
|
const route = useRoute();
|
|
@@ -8,8 +9,10 @@ const table = config?.tables?.pages || "pages";
|
|
|
8
9
|
const bucket = config?.storageBucket || "site";
|
|
9
10
|
const pageId = route.params.id;
|
|
10
11
|
const CMS_COMPONENTS = useAdminSections();
|
|
12
|
+
const { vueApp } = useNuxtApp();
|
|
13
|
+
const resolveSection = (type) => vueApp.component(`Sections${type}`) || vueApp.component(type) || type;
|
|
11
14
|
const { data: page } = await useAsyncData(`admin-edit-${pageId}`, async () => {
|
|
12
|
-
const { data } = await supabase.from(table).select("*").eq("id", pageId).
|
|
15
|
+
const { data } = await supabase.from(table).select("*").eq("id", pageId).maybeSingle();
|
|
13
16
|
return data;
|
|
14
17
|
});
|
|
15
18
|
useHead({ title: computed(() => page.value?.title ? `${page.value.title} \u2014 Editor` : "Editor") });
|
|
@@ -299,7 +302,7 @@ const savePage = async () => {
|
|
|
299
302
|
<div class="max-w-4xl mx-auto bg-white shadow-2xl rounded-[32px] overflow-hidden min-h-screen border border-black/5">
|
|
300
303
|
<div v-for="(block, index) in page.content" :key="index" @click="selectedBlockIndex = index"
|
|
301
304
|
class="relative cursor-pointer" :class="{ 'ring-4 ring-blue-500 ring-inset z-10': selectedBlockIndex === index }">
|
|
302
|
-
<component :is="
|
|
305
|
+
<component :is="resolveSection(block.type)" v-bind="resolvePropsForPreview(block.props)" />
|
|
303
306
|
</div>
|
|
304
307
|
<div v-if="!page.content.length" class="p-20 text-center text-gray-300 italic text-sm">
|
|
305
308
|
Empty page — add a section to get started.
|
|
@@ -1,40 +1,99 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, useHead, useRuntimeConfig, useSupabaseClient } from "#imports";
|
|
3
|
+
useHead({ title: "Tableau de bord \u2014 Admin" });
|
|
2
4
|
const config = useRuntimeConfig().public.adminCms;
|
|
3
5
|
const supabase = useSupabaseClient();
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
return
|
|
8
|
-
|
|
6
|
+
const tables = computed(() => config?.tables || { pages: "pages" });
|
|
7
|
+
const pageTypes = computed(() => {
|
|
8
|
+
const types = config?.pageTypes || [];
|
|
9
|
+
return types.map((pt) => ({
|
|
10
|
+
...pt,
|
|
11
|
+
desc: pt.desc || "",
|
|
12
|
+
svgIcon: pt.svgIcon || ""
|
|
13
|
+
}));
|
|
14
|
+
});
|
|
15
|
+
const defaultIcons = {
|
|
16
|
+
landing_page: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"/>',
|
|
17
|
+
service_page: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>',
|
|
18
|
+
about_page: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>',
|
|
19
|
+
product_page: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>',
|
|
20
|
+
blog_article: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>',
|
|
21
|
+
portfolio_item: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>'
|
|
22
|
+
};
|
|
23
|
+
const { data: stats } = await useAsyncData("admin-dashboard-stats", async () => {
|
|
24
|
+
const pagesTable = tables.value.pages || "pages";
|
|
25
|
+
const [pagesRes, templatesRes] = await Promise.all([
|
|
26
|
+
supabase.from(pagesTable).select("type, status"),
|
|
27
|
+
supabase.from("pages_templates").select("id", { count: "exact", head: true })
|
|
28
|
+
]);
|
|
29
|
+
const pages = pagesRes.data || [];
|
|
30
|
+
const byType = pages.reduce((acc, p) => {
|
|
31
|
+
acc[p.type] = (acc[p.type] || 0) + 1;
|
|
9
32
|
return acc;
|
|
10
|
-
}, {})
|
|
33
|
+
}, {});
|
|
34
|
+
const published = pages.filter((p) => p.status === "published").length;
|
|
35
|
+
const drafts = pages.filter((p) => p.status === "draft").length;
|
|
36
|
+
return { total: pages.length, published, drafts, byType, templates: templatesRes.count ?? 0 };
|
|
11
37
|
});
|
|
12
|
-
useHead({ title: "Dashboard \u2014 Admin" });
|
|
13
38
|
</script>
|
|
14
39
|
|
|
15
40
|
<template>
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
41
|
+
<AdminDashboardLayout>
|
|
42
|
+
<div class="p-8 max-w-5xl mx-auto">
|
|
43
|
+
|
|
44
|
+
<!-- Header -->
|
|
45
|
+
<div class="mb-8">
|
|
46
|
+
<h1 class="text-2xl font-black text-gray-900">Tableau de bord</h1>
|
|
47
|
+
<p class="text-sm text-gray-400 mt-1">Gérez votre contenu et vos pages.</p>
|
|
48
|
+
</div>
|
|
21
49
|
|
|
22
|
-
|
|
50
|
+
<!-- Stats row -->
|
|
51
|
+
<div class="grid grid-cols-4 gap-4 mb-10">
|
|
52
|
+
<div class="bg-white rounded-2xl p-5 border border-gray-100 shadow-sm">
|
|
53
|
+
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2">Total pages</p>
|
|
54
|
+
<p class="text-3xl font-black text-gray-900">{{ stats?.total ?? 0 }}</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="bg-white rounded-2xl p-5 border border-gray-100 shadow-sm">
|
|
57
|
+
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2">Publiées</p>
|
|
58
|
+
<p class="text-3xl font-black text-green-600">{{ stats?.published ?? 0 }}</p>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="bg-white rounded-2xl p-5 border border-gray-100 shadow-sm">
|
|
61
|
+
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2">Brouillons</p>
|
|
62
|
+
<p class="text-3xl font-black text-amber-500">{{ stats?.drafts ?? 0 }}</p>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="bg-white rounded-2xl p-5 border border-gray-100 shadow-sm">
|
|
65
|
+
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2">Templates</p>
|
|
66
|
+
<p class="text-3xl font-black text-[#3d35ff]">{{ stats?.templates ?? 0 }}</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Page types grid -->
|
|
71
|
+
<div class="mb-3 flex items-center justify-between">
|
|
72
|
+
<h2 class="text-[11px] font-black uppercase tracking-widest text-gray-400">Types de pages</h2>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
23
75
|
<NuxtLink
|
|
24
76
|
v-for="type in pageTypes"
|
|
25
77
|
:key="type.id"
|
|
26
78
|
:to="`/admin/pages/${type.id}`"
|
|
27
|
-
class="bg-white p-
|
|
79
|
+
class="group bg-white rounded-2xl p-6 border border-gray-100 shadow-sm hover:border-[#3d35ff]/30 hover:shadow-md transition-all"
|
|
28
80
|
>
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
<div class="flex items-start justify-between mb-4">
|
|
82
|
+
<div class="w-9 h-9 rounded-xl bg-gray-50 group-hover:bg-[#3d35ff]/10 flex items-center justify-center transition-colors">
|
|
83
|
+
<svg class="text-gray-400 group-hover:text-[#3d35ff] transition-colors" style="width:18px;height:18px" fill="none" stroke="currentColor" viewBox="0 0 24 24" v-html="type.svgIcon || defaultIcons[type.id] || defaultIcons.blog_article"></svg>
|
|
84
|
+
</div>
|
|
85
|
+
<span class="text-[10px] font-black px-2 py-1 rounded-full bg-gray-50 text-gray-400">
|
|
86
|
+
{{ stats?.byType?.[type.id] ?? 0 }}
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
<h3 class="font-black text-gray-900 text-sm group-hover:text-[#3d35ff] transition-colors">{{ type.label }}</h3>
|
|
90
|
+
<p class="text-[11px] text-gray-400 mt-1">{{ type.desc }}</p>
|
|
91
|
+
<div class="mt-4 flex items-center gap-1 text-[10px] font-bold text-[#3d35ff] opacity-0 group-hover:opacity-100 transition-opacity">
|
|
92
|
+
Gérer <span>→</span>
|
|
93
|
+
</div>
|
|
36
94
|
</NuxtLink>
|
|
37
95
|
</div>
|
|
96
|
+
|
|
38
97
|
</div>
|
|
39
|
-
</
|
|
98
|
+
</AdminDashboardLayout>
|
|
40
99
|
</template>
|