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.
Files changed (68) hide show
  1. package/dist/module.d.mts +17 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +19 -11
  4. package/dist/runtime/assets/admin-tw.css +1 -1
  5. package/dist/runtime/components/admin/ContentCalendar.d.vue.ts +3 -0
  6. package/dist/runtime/components/admin/ContentCalendar.vue +196 -0
  7. package/dist/runtime/components/admin/ContentCalendar.vue.d.ts +3 -0
  8. package/dist/runtime/components/admin/DashboardLayout.d.vue.ts +13 -0
  9. package/dist/runtime/components/admin/DashboardLayout.vue +12 -0
  10. package/dist/runtime/components/admin/DashboardLayout.vue.d.ts +13 -0
  11. package/dist/runtime/components/admin/NavItem.d.vue.ts +3 -0
  12. package/dist/runtime/components/admin/NavItem.vue +46 -0
  13. package/dist/runtime/components/admin/NavItem.vue.d.ts +3 -0
  14. package/dist/runtime/components/admin/Navbar.vue +1 -1
  15. package/dist/runtime/components/admin/Sidebar.d.vue.ts +3 -0
  16. package/dist/runtime/components/admin/Sidebar.vue +115 -0
  17. package/dist/runtime/components/admin/Sidebar.vue.d.ts +3 -0
  18. package/dist/runtime/composables/useAdminBranding.d.ts +4 -0
  19. package/dist/runtime/composables/useAdminBranding.js +22 -0
  20. package/dist/runtime/composables/useAdminContent.d.ts +9 -0
  21. package/dist/runtime/composables/useAdminContent.js +46 -0
  22. package/dist/runtime/composables/useAdminPersonas.d.ts +8 -0
  23. package/dist/runtime/composables/useAdminPersonas.js +31 -0
  24. package/dist/runtime/composables/useAdminProducts.d.ts +6 -0
  25. package/dist/runtime/composables/useAdminProducts.js +24 -0
  26. package/dist/runtime/composables/useAdminSections.js +1 -0
  27. package/dist/runtime/composables/useAdminStrategies.d.ts +10 -0
  28. package/dist/runtime/composables/useAdminStrategies.js +57 -0
  29. package/dist/runtime/composables/useAdminTodos.d.ts +10 -0
  30. package/dist/runtime/composables/useAdminTodos.js +55 -0
  31. package/dist/runtime/middleware/admin-auth.js +2 -0
  32. package/dist/runtime/pages/admin/account.vue +1 -0
  33. package/dist/runtime/pages/admin/branding.d.vue.ts +3 -0
  34. package/dist/runtime/pages/admin/branding.vue +276 -0
  35. package/dist/runtime/pages/admin/branding.vue.d.ts +3 -0
  36. package/dist/runtime/pages/admin/calendar.d.vue.ts +3 -0
  37. package/dist/runtime/pages/admin/calendar.vue +169 -0
  38. package/dist/runtime/pages/admin/calendar.vue.d.ts +3 -0
  39. package/dist/runtime/pages/admin/content-library.d.vue.ts +3 -0
  40. package/dist/runtime/pages/admin/content-library.vue +359 -0
  41. package/dist/runtime/pages/admin/content-library.vue.d.ts +3 -0
  42. package/dist/runtime/pages/admin/editor/[id].vue +5 -2
  43. package/dist/runtime/pages/admin/index.vue +81 -22
  44. package/dist/runtime/pages/admin/pages/[type].vue +1 -0
  45. package/dist/runtime/pages/admin/personas/[id].d.vue.ts +3 -0
  46. package/dist/runtime/pages/admin/personas/[id].vue +170 -0
  47. package/dist/runtime/pages/admin/personas/[id].vue.d.ts +3 -0
  48. package/dist/runtime/pages/admin/personas/index.d.vue.ts +3 -0
  49. package/dist/runtime/pages/admin/personas/index.vue +266 -0
  50. package/dist/runtime/pages/admin/personas/index.vue.d.ts +3 -0
  51. package/dist/runtime/pages/admin/products.d.vue.ts +3 -0
  52. package/dist/runtime/pages/admin/products.vue +300 -0
  53. package/dist/runtime/pages/admin/products.vue.d.ts +3 -0
  54. package/dist/runtime/pages/admin/strategies/[id].d.vue.ts +3 -0
  55. package/dist/runtime/pages/admin/strategies/[id].vue +237 -0
  56. package/dist/runtime/pages/admin/strategies/[id].vue.d.ts +3 -0
  57. package/dist/runtime/pages/admin/strategies/index.d.vue.ts +3 -0
  58. package/dist/runtime/pages/admin/strategies/index.vue +232 -0
  59. package/dist/runtime/pages/admin/strategies/index.vue.d.ts +3 -0
  60. package/dist/runtime/pages/admin/todos.d.vue.ts +3 -0
  61. package/dist/runtime/pages/admin/todos.vue +251 -0
  62. package/dist/runtime/pages/admin/todos.vue.d.ts +3 -0
  63. package/dist/runtime/plugins/admin-font.client.d.ts +0 -5
  64. package/dist/runtime/plugins/admin-font.client.js +1 -0
  65. package/dist/runtime/stores/adminUser.d.ts +29 -1
  66. package/dist/runtime/stores/adminUser.js +4 -2
  67. package/dist/types.d.mts +1 -1
  68. package/package.json +6 -3
@@ -0,0 +1,232 @@
1
+ <script setup>
2
+ import { ref, computed, onMounted, useHead, navigateTo } from "#imports";
3
+ import { useAdminStrategies } from "#imports";
4
+ useHead({ title: "Strategies - Admin" });
5
+ const { fetchStrategies, createStrategy, updateStrategy, deleteStrategy, fetchStrategyContents, fetchStrategyPersonas } = useAdminStrategies();
6
+ const strategies = ref([]);
7
+ const strategyContents = ref({});
8
+ const strategyPersonas = ref({});
9
+ const loading = ref(true);
10
+ const showModal = ref(false);
11
+ const saving = ref(false);
12
+ const editingStrategy = ref(null);
13
+ const defaultForm = () => ({
14
+ name: "",
15
+ description: "",
16
+ objective: "",
17
+ start_date: "",
18
+ end_date: "",
19
+ budget: 0,
20
+ status: "draft",
21
+ priority: "Moyenne",
22
+ completion_percentage: 0
23
+ });
24
+ const formData = ref(defaultForm());
25
+ onMounted(async () => {
26
+ loading.value = true;
27
+ try {
28
+ strategies.value = await fetchStrategies() || [];
29
+ for (const s of strategies.value) {
30
+ const [c, p] = await Promise.all([fetchStrategyContents(s.id), fetchStrategyPersonas(s.id)]);
31
+ strategyContents.value[s.id] = c;
32
+ strategyPersonas.value[s.id] = p;
33
+ }
34
+ } catch {
35
+ } finally {
36
+ loading.value = false;
37
+ }
38
+ });
39
+ const stats = computed(() => [
40
+ { label: "Total", value: strategies.value.length, icon: "mdi:strategy", bg: "bg-blue-50", color: "text-blue-600" },
41
+ { label: "Actives", value: strategies.value.filter((s) => s.status === "active").length, icon: "mdi:rocket-launch", bg: "bg-emerald-50", color: "text-emerald-600" },
42
+ { label: "Completees", value: strategies.value.filter((s) => s.status === "completed").length, icon: "mdi:check-circle", bg: "bg-blue-50", color: "text-blue-600" },
43
+ { label: "En pause", value: strategies.value.filter((s) => s.status === "paused").length, icon: "mdi:pause-circle", bg: "bg-amber-50", color: "text-amber-600" }
44
+ ]);
45
+ const statusClass = (s) => ({
46
+ draft: "bg-slate-100 text-slate-600",
47
+ active: "bg-emerald-100 text-emerald-700",
48
+ paused: "bg-amber-100 text-amber-700",
49
+ completed: "bg-blue-100 text-blue-700",
50
+ cancelled: "bg-red-100 text-red-700"
51
+ })[s] || "bg-slate-100 text-slate-600";
52
+ const formatDate = (d) => d ? new Date(d).toLocaleDateString("fr-FR") : "-";
53
+ const formatBudget = (n) => n ? new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(n) : "-";
54
+ const openCreate = () => {
55
+ editingStrategy.value = null;
56
+ formData.value = defaultForm();
57
+ showModal.value = true;
58
+ };
59
+ const openEdit = (s) => {
60
+ editingStrategy.value = s;
61
+ formData.value = { ...s };
62
+ showModal.value = true;
63
+ };
64
+ const closeModal = () => {
65
+ showModal.value = false;
66
+ editingStrategy.value = null;
67
+ };
68
+ const saveStrategy = async () => {
69
+ if (!formData.value.name) return;
70
+ saving.value = true;
71
+ try {
72
+ if (editingStrategy.value) await updateStrategy(editingStrategy.value.id, formData.value);
73
+ else await createStrategy(formData.value);
74
+ strategies.value = await fetchStrategies();
75
+ closeModal();
76
+ } catch {
77
+ alert("Erreur lors de la sauvegarde");
78
+ } finally {
79
+ saving.value = false;
80
+ }
81
+ };
82
+ const handleDelete = async () => {
83
+ if (!confirm("Supprimer cette strategie ?")) return;
84
+ try {
85
+ await deleteStrategy(editingStrategy.value.id);
86
+ strategies.value = await fetchStrategies();
87
+ closeModal();
88
+ } catch {
89
+ }
90
+ };
91
+ </script>
92
+
93
+ <template>
94
+ <AdminDashboardLayout>
95
+ <div class="max-w-7xl mx-auto px-6 md:px-10 py-10">
96
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
97
+ <div>
98
+ <h1 class="text-4xl font-bold text-slate-900">Strategies <span class="text-blue-600">Marketing</span></h1>
99
+ <p class="text-slate-500 mt-1">Gerez vos campagnes avec contenus et personas</p>
100
+ </div>
101
+ <button @click="openCreate" class="bg-blue-600 text-white px-8 py-4 rounded-2xl font-bold text-sm hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center gap-2">
102
+ <Icon name="mdi:plus-circle" class="text-xl" /> Nouvelle Strategie
103
+ </button>
104
+ </div>
105
+
106
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-10">
107
+ <div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
108
+ <div :class="[stat.bg, 'w-10 h-10 rounded-xl flex items-center justify-center mb-4']">
109
+ <Icon :name="stat.icon" :class="[stat.color, 'text-xl']" />
110
+ </div>
111
+ <p class="text-xs font-semibold text-slate-400 uppercase">{{ stat.label }}</p>
112
+ <p class="text-2xl font-bold text-slate-900">{{ stat.value }}</p>
113
+ </div>
114
+ </div>
115
+
116
+ <div class="space-y-6">
117
+ <div v-for="strategy in strategies" :key="strategy.id" class="bg-white rounded-[2rem] border border-slate-200 shadow-sm overflow-hidden hover:shadow-xl transition-all">
118
+ <div class="p-6 border-b border-slate-100">
119
+ <div class="flex items-start justify-between mb-4">
120
+ <div class="flex-1">
121
+ <div class="flex items-center gap-3 mb-2">
122
+ <h3 class="text-xl font-bold text-slate-900">{{ strategy.name }}</h3>
123
+ <span :class="['px-3 py-1 rounded-full text-xs font-semibold', statusClass(strategy.status)]">{{ strategy.status }}</span>
124
+ </div>
125
+ <p class="text-sm text-slate-600">{{ strategy.description }}</p>
126
+ </div>
127
+ <div class="flex gap-2 ml-4">
128
+ <button @click="navigateTo(`/admin/strategies/${strategy.id}`)" class="px-5 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-semibold hover:bg-blue-700 transition-all">Gerer</button>
129
+ <button @click="openEdit(strategy)" class="p-2.5 bg-slate-100 text-slate-600 rounded-xl hover:bg-slate-200 transition-all"><Icon name="mdi:pencil" /></button>
130
+ </div>
131
+ </div>
132
+ <div class="mb-4">
133
+ <div class="flex justify-between mb-1.5 text-xs">
134
+ <span class="font-medium text-slate-500">Progression</span>
135
+ <span class="font-semibold text-blue-600">{{ strategy.completion_percentage || 0 }}%</span>
136
+ </div>
137
+ <div class="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
138
+ <div :style="{ width: `${strategy.completion_percentage || 0}%` }" class="h-full bg-gradient-to-r from-blue-500 to-pink-500 transition-all duration-500"></div>
139
+ </div>
140
+ </div>
141
+ <div class="flex items-center gap-4 text-xs text-slate-500">
142
+ <span v-if="strategy.start_date">{{ formatDate(strategy.start_date) }}</span>
143
+ <span v-if="strategy.budget">{{ formatBudget(strategy.budget) }}</span>
144
+ </div>
145
+ </div>
146
+ <div class="grid grid-cols-2 gap-px bg-slate-100">
147
+ <div class="bg-white p-4 text-center">
148
+ <p class="text-lg font-bold text-blue-600">{{ strategyContents[strategy.id]?.length || 0 }}</p>
149
+ <p class="text-xs font-medium text-slate-400">Contenus</p>
150
+ </div>
151
+ <div class="bg-white p-4 text-center">
152
+ <p class="text-lg font-bold text-blue-600">{{ strategyPersonas[strategy.id]?.length || 0 }}</p>
153
+ <p class="text-xs font-medium text-slate-400">Personas</p>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <div v-if="!loading && strategies.length === 0" class="text-center py-20 bg-white rounded-3xl border border-slate-200">
159
+ <Icon name="mdi:strategy" class="text-6xl text-slate-200 mb-4" />
160
+ <p class="text-slate-400 font-medium mb-4">Aucune strategie. Creez-en une pour commencer!</p>
161
+ <button @click="openCreate" class="px-6 py-3 bg-blue-600 text-white rounded-xl font-bold">Creer ma premiere strategie</button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <Teleport to="body">
167
+ <div v-if="showModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-md z-50 flex items-center justify-center p-6" @click.self="closeModal">
168
+ <div class="bg-white rounded-[3rem] max-w-3xl w-full max-h-[90vh] overflow-hidden shadow-2xl">
169
+ <div class="bg-blue-600 p-8 text-white flex justify-between items-center">
170
+ <h2 class="text-2xl font-bold">{{ editingStrategy ? "Modifier Strategie" : "Nouvelle Strategie" }}</h2>
171
+ <button @click="closeModal" class="w-12 h-12 bg-white/10 hover:bg-white/20 rounded-2xl flex items-center justify-center">
172
+ <Icon name="mdi:close" class="text-2xl" />
173
+ </button>
174
+ </div>
175
+ <div class="p-10 overflow-y-auto max-h-[calc(90vh-200px)] space-y-6">
176
+ <div>
177
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Nom *</label>
178
+ <input v-model="formData.name" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-medium" placeholder="Ex: Campagne Instagram Q1 2024" />
179
+ </div>
180
+ <div>
181
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Description</label>
182
+ <textarea v-model="formData.description" rows="3" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-medium" placeholder="Decrivez votre strategie..."></textarea>
183
+ </div>
184
+ <div>
185
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Objectif</label>
186
+ <textarea v-model="formData.objective" rows="2" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-medium" placeholder="Quel est l'objectif principal?"></textarea>
187
+ </div>
188
+ <div class="grid grid-cols-2 gap-4">
189
+ <div>
190
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Date debut</label>
191
+ <input v-model="formData.start_date" type="date" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-bold" />
192
+ </div>
193
+ <div>
194
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Date fin</label>
195
+ <input v-model="formData.end_date" type="date" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-bold" />
196
+ </div>
197
+ </div>
198
+ <div>
199
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Budget (EUR)</label>
200
+ <input v-model.number="formData.budget" type="number" step="0.01" class="w-full px-6 py-4 bg-emerald-50 border-none rounded-2xl font-bold text-emerald-900" />
201
+ </div>
202
+ <div>
203
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Statut</label>
204
+ <div class="grid grid-cols-4 gap-2">
205
+ <button v-for="s in ['draft', 'active', 'paused', 'completed']" :key="s" type="button" @click="formData.status = s"
206
+ :class="[formData.status === s ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-500']"
207
+ class="py-3 rounded-xl text-xs font-black uppercase transition-all">{{ s }}</button>
208
+ </div>
209
+ </div>
210
+ <div>
211
+ <label class="block text-xs font-black uppercase text-slate-400 mb-2">Priorite</label>
212
+ <div class="grid grid-cols-4 gap-2">
213
+ <button v-for="p in ['Basse', 'Moyenne', 'Haute', 'Critique']" :key="p" type="button" @click="formData.priority = p"
214
+ :class="[formData.priority === p ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500']"
215
+ class="py-3 rounded-xl text-xs font-black uppercase">{{ p }}</button>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ <div class="p-8 bg-slate-50 border-t flex justify-between">
220
+ <button v-if="editingStrategy" @click="handleDelete" class="px-6 py-3 bg-red-100 text-red-600 rounded-2xl font-semibold text-sm">Supprimer</button>
221
+ <div class="flex gap-4 ml-auto">
222
+ <button @click="closeModal" class="px-8 py-4 rounded-2xl font-medium text-sm text-slate-500 hover:bg-slate-200">Annuler</button>
223
+ <button @click="saveStrategy" :disabled="saving" class="bg-blue-600 text-white px-10 py-4 rounded-2xl font-semibold text-sm shadow-xl disabled:opacity-50">
224
+ {{ saving ? "Enregistrement..." : "Enregistrer" }}
225
+ </button>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </Teleport>
231
+ </AdminDashboardLayout>
232
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,251 @@
1
+ <script setup>
2
+ import { ref, computed, onMounted, useHead } from "#imports";
3
+ import { useAdminTodos } from "#imports";
4
+ useHead({ title: "Todos - Admin" });
5
+ const { fetchTodos, toggleTodo: toggleTodoDb, createTodo, updateTodo, deleteTodo } = useAdminTodos();
6
+ const todos = ref([]);
7
+ const loading = ref(true);
8
+ const showModal = ref(false);
9
+ const saving = ref(false);
10
+ const editingTodo = ref(null);
11
+ const formData = ref({ title: "", description: "", priority: "Moyenne", due_date: "" });
12
+ onMounted(async () => {
13
+ loading.value = true;
14
+ try {
15
+ todos.value = await fetchTodos();
16
+ } catch {
17
+ } finally {
18
+ loading.value = false;
19
+ }
20
+ });
21
+ const stats = computed(() => {
22
+ const active = todos.value.filter((t) => !t.completed).length;
23
+ const completed = todos.value.filter((t) => t.completed).length;
24
+ const overdue = todos.value.filter((t) => isOverdue(t)).length;
25
+ const total = todos.value.length || 1;
26
+ return [
27
+ { label: "Actifs", value: active, icon: "mdi:checkbox-marked-circle", bg: "bg-blue-50", color: "text-blue-600" },
28
+ { label: "Termines", value: completed, icon: "mdi:check-all", bg: "bg-emerald-50", color: "text-emerald-600" },
29
+ { label: "En Retard", value: overdue, icon: "mdi:alert-circle", bg: "bg-red-50", color: "text-red-600" },
30
+ { label: "Progres", value: Math.round(completed / total * 100) + "%", icon: "mdi:chart-arc", bg: "bg-blue-50", color: "text-blue-600" }
31
+ ];
32
+ });
33
+ const todosByDate = computed(() => {
34
+ const grouped = {};
35
+ const today = /* @__PURE__ */ new Date();
36
+ today.setHours(0, 0, 0, 0);
37
+ const tomorrow = new Date(today);
38
+ tomorrow.setDate(tomorrow.getDate() + 1);
39
+ todos.value.forEach((todo) => {
40
+ if (!todo.due_date) return;
41
+ const d = new Date(todo.due_date);
42
+ d.setHours(0, 0, 0, 0);
43
+ let label = d < today ? "En Retard" : d.getTime() === today.getTime() ? "Aujourd'hui" : d.getTime() === tomorrow.getTime() ? "Demain" : d.getTime() < today.getTime() + 7 * 864e5 ? "Cette Semaine" : "Plus Tard";
44
+ if (!grouped[label]) grouped[label] = [];
45
+ grouped[label].push(todo);
46
+ });
47
+ return grouped;
48
+ });
49
+ const todosWithoutDate = computed(
50
+ () => todos.value.filter((t) => !t.due_date).sort((a, b) => {
51
+ const o = { Haute: 0, Moyenne: 1, Basse: 2 };
52
+ return (o[a.priority] ?? 2) - (o[b.priority] ?? 2);
53
+ })
54
+ );
55
+ const isOverdue = (todo) => {
56
+ if (!todo.due_date || todo.completed) return false;
57
+ return new Date(todo.due_date) < /* @__PURE__ */ new Date();
58
+ };
59
+ const formatDueDate = (d) => {
60
+ const date = new Date(d);
61
+ const diff = Math.floor((date.getTime() - Date.now()) / 864e5);
62
+ if (diff < 0) return `Il y a ${Math.abs(diff)}j`;
63
+ if (diff === 0) return "Aujourd'hui " + date.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
64
+ if (diff === 1) return "Demain";
65
+ return date.toLocaleDateString("fr-FR", { day: "numeric", month: "short" });
66
+ };
67
+ const getPriorityBorder = (priority, overdue) => {
68
+ if (overdue) return "border-l-red-600 bg-red-50";
69
+ if (priority === "Haute") return "border-l-red-500 bg-white";
70
+ if (priority === "Moyenne") return "border-l-amber-500 bg-white";
71
+ return "border-l-slate-400 bg-slate-50";
72
+ };
73
+ const getLabelColor = (label) => {
74
+ if (label.includes("Retard")) return "text-red-700";
75
+ if (label.includes("Aujourd'hui")) return "text-blue-700";
76
+ if (label === "Demain") return "text-amber-700";
77
+ return "text-slate-700";
78
+ };
79
+ const toggleTodo = async (todo) => {
80
+ try {
81
+ await toggleTodoDb(todo.id);
82
+ todos.value = await fetchTodos();
83
+ } catch {
84
+ }
85
+ };
86
+ const openCreate = () => {
87
+ editingTodo.value = null;
88
+ formData.value = { title: "", description: "", priority: "Moyenne", due_date: "" };
89
+ showModal.value = true;
90
+ };
91
+ const openEdit = (todo) => {
92
+ editingTodo.value = todo;
93
+ let due = "";
94
+ if (todo.due_date) {
95
+ const d = new Date(todo.due_date);
96
+ due = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}T${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
97
+ }
98
+ formData.value = { ...todo, due_date: due };
99
+ showModal.value = true;
100
+ };
101
+ const closeModal = () => {
102
+ showModal.value = false;
103
+ editingTodo.value = null;
104
+ };
105
+ const saveTodo = async () => {
106
+ if (!formData.value.title) return;
107
+ saving.value = true;
108
+ try {
109
+ const data = { ...formData.value };
110
+ data.due_date = data.due_date ? new Date(data.due_date).toISOString() : null;
111
+ if (editingTodo.value) await updateTodo(editingTodo.value.id, data);
112
+ else await createTodo(data);
113
+ todos.value = await fetchTodos();
114
+ closeModal();
115
+ } catch {
116
+ } finally {
117
+ saving.value = false;
118
+ }
119
+ };
120
+ const handleDelete = async (todo) => {
121
+ if (confirm("Supprimer ?")) {
122
+ try {
123
+ await deleteTodo(todo.id);
124
+ todos.value = await fetchTodos();
125
+ } catch {
126
+ }
127
+ }
128
+ };
129
+ </script>
130
+
131
+ <template>
132
+ <AdminDashboardLayout>
133
+ <div class="max-w-7xl mx-auto px-6 md:px-10 py-10">
134
+ <div class="flex items-center justify-between mb-8">
135
+ <div>
136
+ <h1 class="text-4xl font-bold text-slate-900 mb-1">Todos</h1>
137
+ <p class="text-slate-500">Gerez vos taches et priorites</p>
138
+ </div>
139
+ <button @click="openCreate" class="px-6 py-3 bg-slate-900 text-white rounded-2xl font-bold text-sm hover:bg-slate-800 transition-all flex items-center gap-2">
140
+ <Icon name="mdi:plus-circle" class="text-xl" />
141
+ Nouveau Todo
142
+ </button>
143
+ </div>
144
+
145
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-10">
146
+ <div v-for="stat in stats" :key="stat.label" class="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm">
147
+ <div :class="[stat.bg, 'w-10 h-10 rounded-xl flex items-center justify-center mb-4']">
148
+ <Icon :name="stat.icon" :class="[stat.color, 'text-xl']" />
149
+ </div>
150
+ <p class="text-xs font-bold text-slate-400 uppercase">{{ stat.label }}</p>
151
+ <p class="text-3xl font-black text-slate-900">{{ stat.value }}</p>
152
+ </div>
153
+ </div>
154
+
155
+ <div v-if="loading" class="text-center py-20 text-slate-400">
156
+ <Icon name="mdi:loading" class="text-5xl animate-spin" />
157
+ </div>
158
+
159
+ <div v-else class="space-y-8">
160
+ <div v-for="(group, label) in todosByDate" :key="label">
161
+ <div class="flex items-center gap-3 mb-3">
162
+ <h3 :class="['text-sm font-black uppercase tracking-tight', getLabelColor(label)]">{{ label }}</h3>
163
+ <div class="h-px flex-1 bg-slate-200"></div>
164
+ <span class="text-xs font-bold text-slate-400">{{ group.length }}</span>
165
+ </div>
166
+ <div class="space-y-2">
167
+ <div v-for="todo in group" :key="todo.id"
168
+ :class="['rounded-xl p-3 shadow-sm hover:shadow-md transition-all group border-l-4', getPriorityBorder(todo.priority, isOverdue(todo))]">
169
+ <div class="flex items-center gap-3">
170
+ <button @click="toggleTodo(todo)" :class="['w-5 h-5 rounded-lg border-2 transition-all flex-shrink-0', todo.completed ? 'bg-emerald-500 border-emerald-500' : 'bg-white border-slate-200']">
171
+ <Icon v-if="todo.completed" name="mdi:check-bold" class="text-white text-xs m-auto" />
172
+ </button>
173
+ <div class="flex-1 min-w-0">
174
+ <div class="flex items-center gap-2">
175
+ <p :class="['text-sm truncate', todo.completed ? 'line-through text-slate-400' : 'text-slate-900 font-bold']">{{ todo.title }}</p>
176
+ <span v-if="todo.priority" :class="['text-[9px] font-black px-1.5 py-0.5 rounded-full uppercase flex-shrink-0', todo.priority === 'Haute' ? 'bg-red-100 text-red-700' : todo.priority === 'Moyenne' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-600']">{{ todo.priority }}</span>
177
+ <span v-if="isOverdue(todo) && !todo.completed" class="px-1.5 py-0.5 bg-red-600 text-white rounded-full text-[9px] font-black uppercase animate-pulse flex-shrink-0">Retard</span>
178
+ </div>
179
+ <p v-if="todo.description" class="text-slate-500 text-xs line-clamp-1">{{ todo.description }}</p>
180
+ <p v-if="todo.due_date" :class="['text-[10px] mt-0.5', isOverdue(todo) && !todo.completed ? 'text-red-600 font-bold' : 'text-slate-400']">{{ formatDueDate(todo.due_date) }}</p>
181
+ </div>
182
+ <div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
183
+ <button @click="openEdit(todo)" class="p-1.5 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-600 hover:text-white transition-all"><Icon name="mdi:pencil" class="text-sm" /></button>
184
+ <button @click="handleDelete(todo)" class="p-1.5 bg-red-50 text-red-600 rounded-lg hover:bg-red-600 hover:text-white transition-all"><Icon name="mdi:delete" class="text-sm" /></button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ <div v-if="todosWithoutDate.length > 0">
192
+ <div class="flex items-center gap-3 mb-3">
193
+ <h3 class="text-sm font-black text-slate-400 uppercase tracking-tight">Sans Date</h3>
194
+ <div class="h-px flex-1 bg-slate-200"></div>
195
+ <span class="text-xs font-bold text-slate-400">{{ todosWithoutDate.length }}</span>
196
+ </div>
197
+ <div class="space-y-2">
198
+ <div v-for="todo in todosWithoutDate" :key="todo.id" class="bg-slate-50 rounded-xl p-3 shadow-sm hover:shadow-md transition-all group border-l-4 border-l-slate-300">
199
+ <div class="flex items-center gap-3">
200
+ <button @click="toggleTodo(todo)" :class="['w-5 h-5 rounded-lg border-2 transition-all flex-shrink-0', todo.completed ? 'bg-emerald-500 border-emerald-500' : 'bg-white border-slate-200']">
201
+ <Icon v-if="todo.completed" name="mdi:check-bold" class="text-white text-xs m-auto" />
202
+ </button>
203
+ <div class="flex-1 min-w-0">
204
+ <p :class="['text-sm truncate', todo.completed ? 'line-through text-slate-400' : 'text-slate-900 font-bold']">{{ todo.title }}</p>
205
+ <p v-if="todo.description" class="text-slate-500 text-xs line-clamp-1">{{ todo.description }}</p>
206
+ </div>
207
+ <div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
208
+ <button @click="openEdit(todo)" class="p-1.5 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-600 hover:text-white transition-all"><Icon name="mdi:pencil" class="text-sm" /></button>
209
+ <button @click="handleDelete(todo)" class="p-1.5 bg-red-50 text-red-600 rounded-lg hover:bg-red-600 hover:text-white transition-all"><Icon name="mdi:delete" class="text-sm" /></button>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ <div v-if="todos.length === 0" class="text-center py-20 bg-white rounded-3xl border-2 border-dashed border-slate-200">
217
+ <Icon name="mdi:clipboard-text-outline" class="text-6xl text-slate-200 mb-4" />
218
+ <p class="text-xl font-bold text-slate-800">Aucun todo pour le moment</p>
219
+ <button @click="openCreate" class="text-blue-600 font-black hover:underline mt-2">+ Creer votre premiere tache</button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <Teleport to="body">
225
+ <div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-md">
226
+ <div class="bg-white rounded-[2rem] w-full max-w-lg overflow-hidden shadow-2xl">
227
+ <div class="p-5 border-b border-slate-100 bg-slate-50 flex justify-between items-center">
228
+ <h2 class="text-xl font-black text-slate-900">{{ editingTodo ? "Modifier" : "Nouveau Todo" }}</h2>
229
+ <button @click="closeModal" class="text-slate-400 hover:text-slate-600"><Icon name="mdi:close" class="text-xl" /></button>
230
+ </div>
231
+ <div class="p-5 space-y-4">
232
+ <input v-model="formData.title" type="text" placeholder="Titre..." class="w-full px-4 py-3 bg-slate-50 rounded-xl border-none focus:ring-2 focus:ring-blue-500 font-bold text-sm" />
233
+ <textarea v-model="formData.description" rows="2" placeholder="Description..." class="w-full px-4 py-3 bg-slate-50 rounded-xl border-none focus:ring-2 focus:ring-blue-500 font-medium resize-none text-sm"></textarea>
234
+ <div>
235
+ <label class="block text-[10px] font-bold text-slate-400 uppercase mb-1.5 ml-1">Echeance</label>
236
+ <input v-model="formData.due_date" type="datetime-local" class="w-full px-4 py-3 bg-emerald-50 border-2 border-emerald-200 rounded-xl font-bold text-emerald-900 text-sm" />
237
+ </div>
238
+ <div class="grid grid-cols-3 gap-1.5 p-1 bg-slate-100 rounded-xl">
239
+ <button v-for="p in ['Basse', 'Moyenne', 'Haute']" :key="p" @click="formData.priority = p"
240
+ :class="formData.priority === p ? 'bg-white shadow-sm text-slate-900' : 'text-slate-400'"
241
+ class="py-2 rounded-lg font-black text-[10px] uppercase transition-all">{{ p }}</button>
242
+ </div>
243
+ <button @click="saveTodo" :disabled="saving" class="w-full bg-slate-900 text-white font-black py-3 rounded-xl hover:bg-slate-800 transition-all text-sm">
244
+ {{ saving ? "CHARGEMENT..." : "VALIDER" }}
245
+ </button>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </Teleport>
250
+ </AdminDashboardLayout>
251
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -1,7 +1,2 @@
1
- /**
2
- * Captures the host project's body font-family BEFORE the admin scope
3
- * (data-admin-cms) can shadow it, then stores it in --admin-site-font so
4
- * the section preview area can revert to the correct host font.
5
- */
6
1
  declare const _default: any;
7
2
  export default _default;
@@ -1,3 +1,4 @@
1
+ import { defineNuxtPlugin } from "#imports";
1
2
  export default defineNuxtPlugin(() => {
2
3
  const bodyFont = window.getComputedStyle(document.body).fontFamily;
3
4
  if (bodyFont) {
@@ -1 +1,29 @@
1
- export declare const useAdminUserStore: any;
1
+ export declare const useAdminUserStore: import("pinia").StoreDefinition<"adminUser", {
2
+ userData: Record<string, any> | null;
3
+ loading: boolean;
4
+ error: string | null;
5
+ }, {
6
+ isSubscriptionActive: (state: {
7
+ userData: Record<string, any> | null;
8
+ loading: boolean;
9
+ error: string | null;
10
+ } & import("pinia").PiniaCustomStateProperties<{
11
+ userData: Record<string, any> | null;
12
+ loading: boolean;
13
+ error: string | null;
14
+ }>) => any;
15
+ trialDaysRemaining: (state: {
16
+ userData: Record<string, any> | null;
17
+ loading: boolean;
18
+ error: string | null;
19
+ } & import("pinia").PiniaCustomStateProperties<{
20
+ userData: Record<string, any> | null;
21
+ loading: boolean;
22
+ error: string | null;
23
+ }>) => number;
24
+ }, {
25
+ fetchUserData(userId: string): Promise<void>;
26
+ signOut(): Promise<void>;
27
+ clearUserData(): void;
28
+ updateUserData(data: Record<string, any>): void;
29
+ }>;
@@ -1,4 +1,6 @@
1
1
  import { defineStore } from "pinia";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { useSupabaseClient } from "#imports";
2
4
  export const useAdminUserStore = defineStore("adminUser", {
3
5
  state: () => ({
4
6
  userData: null,
@@ -24,7 +26,7 @@ export const useAdminUserStore = defineStore("adminUser", {
24
26
  const table = config?.tables?.users || "users";
25
27
  const { data, error } = await supabase.from(table).select("*").eq("id", userId).single();
26
28
  if (error) throw error;
27
- this.userData = data;
29
+ this.userData = data ? { ...data } : null;
28
30
  } catch (e) {
29
31
  this.error = e.message;
30
32
  } finally {
@@ -49,7 +51,7 @@ export const useAdminUserStore = defineStore("adminUser", {
49
51
  this.error = null;
50
52
  },
51
53
  updateUserData(data) {
52
- this.userData = data;
54
+ this.userData = data ? { ...data } : null;
53
55
  }
54
56
  }
55
57
  });
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default } from './module.mjs'
2
2
 
3
- export { type ModuleOptions, type PageTypeConfig, type PlanConfig, type SectionConfig, type SectionFieldConfig } from './module.mjs'
3
+ export { type ModuleOptions, type NavSectionItem, type PageTypeConfig, type PlanConfig, type SectionConfig, type SectionFieldConfig } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cinqcinqdev-seo",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "A reusable Nuxt 3 admin CMS module with visual page editor powered by Supabase",
5
5
  "license": "MIT",
6
6
  "module": "./dist/module.mjs",
@@ -22,16 +22,19 @@
22
22
  "release": "npm version patch && npm publish"
23
23
  },
24
24
  "peerDependencies": {
25
+ "@nuxt/icon": ">=1.0.0",
25
26
  "@nuxtjs/supabase": ">=1.0.0",
26
- "@pinia/nuxt": ">=0.5.0",
27
+ "@pinia/nuxt": ">=0.5.0 || >=0.11.0",
27
28
  "nuxt": ">=3.0.0"
28
29
  },
29
30
  "devDependencies": {
31
+ "@nuxt/icon": "^1.15.0",
30
32
  "@nuxt/kit": "^3.13.0",
31
33
  "@nuxt/module-builder": "^1.0.2",
32
34
  "@nuxt/schema": "^3.13.0",
33
35
  "@nuxtjs/supabase": "^1.4.0",
34
- "@pinia/nuxt": "^0.9.0",
36
+ "@pinia/nuxt": "^0.11.3",
37
+ "pinia": "^3.0.4",
35
38
  "defu": "^6.1.4",
36
39
  "nuxt": "^3.13.0",
37
40
  "tailwindcss": "^3.4.19",