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,170 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, onMounted, useHead, useRoute, navigateTo } from "#imports";
|
|
3
|
+
import { useAdminPersonas, useAdminContent } from "#imports";
|
|
4
|
+
const route = useRoute();
|
|
5
|
+
const id = route.params.id;
|
|
6
|
+
useHead({ title: "Persona - Admin" });
|
|
7
|
+
const { fetchPersonaById, updatePersona } = useAdminPersonas();
|
|
8
|
+
const { getContentByPersona } = useAdminContent();
|
|
9
|
+
const persona = ref(null);
|
|
10
|
+
const contents = ref([]);
|
|
11
|
+
const loading = ref(true);
|
|
12
|
+
const editMode = ref(false);
|
|
13
|
+
const saving = ref(false);
|
|
14
|
+
const formData = ref({});
|
|
15
|
+
onMounted(async () => {
|
|
16
|
+
loading.value = true;
|
|
17
|
+
try {
|
|
18
|
+
const [p, c] = await Promise.all([fetchPersonaById(id), getContentByPersona(id)]);
|
|
19
|
+
persona.value = p;
|
|
20
|
+
contents.value = c || [];
|
|
21
|
+
formData.value = { ...p };
|
|
22
|
+
} catch {
|
|
23
|
+
} finally {
|
|
24
|
+
loading.value = false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const saveChanges = async () => {
|
|
28
|
+
saving.value = true;
|
|
29
|
+
try {
|
|
30
|
+
const updated = await updatePersona(id, formData.value);
|
|
31
|
+
persona.value = updated;
|
|
32
|
+
editMode.value = false;
|
|
33
|
+
} catch {
|
|
34
|
+
} finally {
|
|
35
|
+
saving.value = false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const getInitials = (name) => name ? name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2) : "?";
|
|
39
|
+
const statusClass = {
|
|
40
|
+
draft: "bg-slate-100 text-slate-600",
|
|
41
|
+
approved: "bg-blue-100 text-blue-600",
|
|
42
|
+
published: "bg-emerald-100 text-emerald-600",
|
|
43
|
+
archived: "bg-amber-100 text-amber-600"
|
|
44
|
+
};
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<AdminDashboardLayout>
|
|
49
|
+
<div class="max-w-5xl mx-auto px-6 md:px-10 py-10">
|
|
50
|
+
<button @click="navigateTo('/admin/personas')" class="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-semibold transition-all mb-8 group">
|
|
51
|
+
<Icon name="mdi:arrow-left" class="text-xl group-hover:-translate-x-1 transition-transform" />
|
|
52
|
+
Retour aux Personas
|
|
53
|
+
</button>
|
|
54
|
+
|
|
55
|
+
<div v-if="loading" class="h-64 bg-slate-100 animate-pulse rounded-3xl"></div>
|
|
56
|
+
|
|
57
|
+
<div v-else-if="persona">
|
|
58
|
+
<!-- Hero -->
|
|
59
|
+
<div class="bg-gradient-to-br from-blue-600 to-blue-800 rounded-3xl p-8 text-white mb-8 relative overflow-hidden">
|
|
60
|
+
<div class="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full blur-3xl -mr-16 -mt-16"></div>
|
|
61
|
+
<div class="relative z-10 flex flex-col md:flex-row items-start gap-8">
|
|
62
|
+
<div class="w-24 h-24 bg-white/20 backdrop-blur-xl rounded-2xl flex items-center justify-center border-2 border-white/40 overflow-hidden flex-shrink-0 text-2xl font-black">
|
|
63
|
+
<img v-if="persona.avatar_url" :src="persona.avatar_url" :alt="persona.name" class="w-full h-full object-cover" />
|
|
64
|
+
<span v-else>{{ getInitials(persona.name) }}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="flex-1">
|
|
67
|
+
<h1 class="text-4xl font-black mb-1">{{ persona.name }}</h1>
|
|
68
|
+
<p class="text-blue-100 text-lg mb-4">{{ persona.job_title }}</p>
|
|
69
|
+
<div class="flex flex-wrap gap-4 text-sm text-blue-100">
|
|
70
|
+
<span v-if="persona.age"><Icon name="mdi:cake-variant" class="inline mr-1" />{{ persona.age }} ans</span>
|
|
71
|
+
<span v-if="persona.location"><Icon name="mdi:map-marker" class="inline mr-1" />{{ persona.location }}</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="flex gap-3">
|
|
75
|
+
<button @click="editMode = !editMode" class="px-5 py-2.5 bg-white/20 hover:bg-white/30 text-white rounded-xl font-bold text-sm transition-all flex items-center gap-2">
|
|
76
|
+
<Icon name="mdi:pencil" /> Modifier
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Edit Form -->
|
|
83
|
+
<div v-if="editMode" class="bg-white rounded-3xl p-8 border border-slate-200 shadow-sm mb-8">
|
|
84
|
+
<h2 class="text-xl font-bold text-slate-900 mb-6">Modifier le Profil</h2>
|
|
85
|
+
<div class="grid grid-cols-2 gap-6 mb-6">
|
|
86
|
+
<div>
|
|
87
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Nom</label>
|
|
88
|
+
<input v-model="formData.name" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Profession</label>
|
|
92
|
+
<input v-model="formData.job_title" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Age</label>
|
|
96
|
+
<input v-model.number="formData.age" type="number" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
|
|
97
|
+
</div>
|
|
98
|
+
<div>
|
|
99
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Ville</label>
|
|
100
|
+
<input v-model="formData.location" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="mb-6">
|
|
104
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">A propos</label>
|
|
105
|
+
<textarea v-model="formData.about" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl resize-none"></textarea>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="flex gap-3">
|
|
108
|
+
<button @click="saveChanges" :disabled="saving" class="px-8 py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 disabled:opacity-50">{{ saving ? "Enregistrement..." : "Enregistrer" }}</button>
|
|
109
|
+
<button @click="editMode = false" class="px-8 py-3 bg-slate-100 text-slate-700 font-bold rounded-xl hover:bg-slate-200">Annuler</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Info Cards -->
|
|
114
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
115
|
+
<div class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
116
|
+
<h3 class="font-black text-slate-900 mb-4 flex items-center gap-2"><Icon name="mdi:account-outline" class="text-blue-600" /> A propos</h3>
|
|
117
|
+
<p class="text-slate-600 leading-relaxed">{{ persona.about || "Aucune description." }}</p>
|
|
118
|
+
</div>
|
|
119
|
+
<div v-if="(persona.core_needs || []).length > 0" class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
120
|
+
<h3 class="font-black text-slate-900 mb-4 flex items-center gap-2"><Icon name="mdi:star-outline" class="text-amber-500" /> Besoins Principaux</h3>
|
|
121
|
+
<div class="flex flex-wrap gap-2">
|
|
122
|
+
<span v-for="need in persona.core_needs" :key="need" class="px-3 py-1.5 bg-blue-50 text-blue-700 rounded-xl text-sm font-semibold">{{ need }}</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div v-if="(persona.goals || []).length > 0" class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
126
|
+
<h3 class="font-black text-slate-900 mb-4 flex items-center gap-2"><Icon name="mdi:target" class="text-emerald-500" /> Objectifs</h3>
|
|
127
|
+
<ul class="space-y-2">
|
|
128
|
+
<li v-for="goal in persona.goals" :key="goal" class="flex items-start gap-2 text-sm text-slate-600">
|
|
129
|
+
<Icon name="mdi:check-circle" class="text-emerald-500 mt-0.5 flex-shrink-0" />{{ goal }}
|
|
130
|
+
</li>
|
|
131
|
+
</ul>
|
|
132
|
+
</div>
|
|
133
|
+
<div v-if="(persona.pain_points || []).length > 0" class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
134
|
+
<h3 class="font-black text-slate-900 mb-4 flex items-center gap-2"><Icon name="mdi:lightning-bolt" class="text-red-500" /> Points de Douleur</h3>
|
|
135
|
+
<ul class="space-y-2">
|
|
136
|
+
<li v-for="pain in persona.pain_points" :key="pain" class="flex items-start gap-2 text-sm text-slate-600">
|
|
137
|
+
<Icon name="mdi:close-circle" class="text-red-400 mt-0.5 flex-shrink-0" />{{ pain }}
|
|
138
|
+
</li>
|
|
139
|
+
</ul>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Recent Content -->
|
|
144
|
+
<div class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
145
|
+
<h3 class="font-black text-slate-900 mb-4 flex items-center gap-2">
|
|
146
|
+
<Icon name="mdi:file-document-multiple-outline" class="text-purple-500" /> Contenus associes ({{ contents.length }})
|
|
147
|
+
</h3>
|
|
148
|
+
<div v-if="contents.length === 0" class="text-center py-8 text-slate-300">
|
|
149
|
+
<Icon name="mdi:file-document-outline" class="text-4xl mb-2" />
|
|
150
|
+
<p class="text-sm">Aucun contenu pour cette persona</p>
|
|
151
|
+
</div>
|
|
152
|
+
<div v-else class="space-y-3">
|
|
153
|
+
<div v-for="content in contents.slice(0, 5)" :key="content.id" class="flex items-center justify-between p-4 bg-slate-50 rounded-xl">
|
|
154
|
+
<div class="flex-1 min-w-0">
|
|
155
|
+
<p class="font-bold text-slate-900 truncate">{{ content.title }}</p>
|
|
156
|
+
<p class="text-xs text-slate-500">{{ content.platform }} • {{ content.created_at ? new Date(content.created_at).toLocaleDateString("fr-FR") : "" }}</p>
|
|
157
|
+
</div>
|
|
158
|
+
<span :class="['px-2 py-1 rounded-lg text-[10px] font-black uppercase', statusClass[content.status] || statusClass.draft]">{{ content.status }}</span>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div v-else class="text-center py-20">
|
|
165
|
+
<Icon name="mdi:account-off" class="text-6xl text-slate-200 mb-4" />
|
|
166
|
+
<p class="text-slate-400">Persona introuvable.</p>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</AdminDashboardLayout>
|
|
170
|
+
</template>
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onMounted, useHead, navigateTo } from "#imports";
|
|
3
|
+
import { useAdminPersonas } from "#imports";
|
|
4
|
+
useHead({ title: "Personas - Admin" });
|
|
5
|
+
const { fetchPersonas, createPersona, updatePersona, deletePersona } = useAdminPersonas();
|
|
6
|
+
const personas = ref([]);
|
|
7
|
+
const loading = ref(true);
|
|
8
|
+
const showModal = ref(false);
|
|
9
|
+
const saving = ref(false);
|
|
10
|
+
const editingPersona = ref(null);
|
|
11
|
+
const currentLayout = ref("table");
|
|
12
|
+
const defaultForm = () => ({
|
|
13
|
+
name: "",
|
|
14
|
+
job_title: "",
|
|
15
|
+
age: null,
|
|
16
|
+
location: "",
|
|
17
|
+
about: "",
|
|
18
|
+
avatar_url: "",
|
|
19
|
+
core_needs: [],
|
|
20
|
+
goals: [],
|
|
21
|
+
pain_points: [],
|
|
22
|
+
frustrations: [],
|
|
23
|
+
preferred_channels: [],
|
|
24
|
+
topics: [],
|
|
25
|
+
gradient_class: ""
|
|
26
|
+
});
|
|
27
|
+
const formData = ref(defaultForm());
|
|
28
|
+
const coreNeedsText = ref("");
|
|
29
|
+
const goalsText = ref("");
|
|
30
|
+
const painPointsText = ref("");
|
|
31
|
+
const layoutIcon = computed(() => currentLayout.value === "table" ? "mdi:table" : "mdi:view-dashboard-variant-outline");
|
|
32
|
+
const layoutLabel = computed(() => currentLayout.value === "table" ? "Tableau" : "Grille");
|
|
33
|
+
onMounted(async () => {
|
|
34
|
+
loading.value = true;
|
|
35
|
+
try {
|
|
36
|
+
personas.value = await fetchPersonas();
|
|
37
|
+
} catch {
|
|
38
|
+
} finally {
|
|
39
|
+
loading.value = false;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const getInitials = (name) => name ? name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2) : "?";
|
|
43
|
+
const openCreate = () => {
|
|
44
|
+
editingPersona.value = null;
|
|
45
|
+
formData.value = defaultForm();
|
|
46
|
+
coreNeedsText.value = "";
|
|
47
|
+
goalsText.value = "";
|
|
48
|
+
painPointsText.value = "";
|
|
49
|
+
showModal.value = true;
|
|
50
|
+
};
|
|
51
|
+
const openEdit = (persona) => {
|
|
52
|
+
editingPersona.value = persona;
|
|
53
|
+
formData.value = { ...defaultForm(), ...persona };
|
|
54
|
+
coreNeedsText.value = (persona.core_needs || []).join("\n");
|
|
55
|
+
goalsText.value = (persona.goals || []).join("\n");
|
|
56
|
+
painPointsText.value = (persona.pain_points || []).join("\n");
|
|
57
|
+
showModal.value = true;
|
|
58
|
+
};
|
|
59
|
+
const closeModal = () => {
|
|
60
|
+
showModal.value = false;
|
|
61
|
+
editingPersona.value = null;
|
|
62
|
+
};
|
|
63
|
+
const savePersona = async () => {
|
|
64
|
+
if (!formData.value.name) return;
|
|
65
|
+
saving.value = true;
|
|
66
|
+
try {
|
|
67
|
+
const data = {
|
|
68
|
+
...formData.value,
|
|
69
|
+
core_needs: coreNeedsText.value.split("\n").map((s) => s.trim()).filter(Boolean),
|
|
70
|
+
goals: goalsText.value.split("\n").map((s) => s.trim()).filter(Boolean),
|
|
71
|
+
pain_points: painPointsText.value.split("\n").map((s) => s.trim()).filter(Boolean)
|
|
72
|
+
};
|
|
73
|
+
if (editingPersona.value) await updatePersona(editingPersona.value.id, data);
|
|
74
|
+
else await createPersona(data);
|
|
75
|
+
personas.value = await fetchPersonas();
|
|
76
|
+
closeModal();
|
|
77
|
+
} catch {
|
|
78
|
+
} finally {
|
|
79
|
+
saving.value = false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const handleDelete = async (id) => {
|
|
83
|
+
if (confirm("Supprimer cette persona ?")) {
|
|
84
|
+
try {
|
|
85
|
+
await deletePersona(id);
|
|
86
|
+
personas.value = await fetchPersonas();
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const viewPersona = (persona) => navigateTo(`/admin/personas/${persona.id}`);
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<AdminDashboardLayout>
|
|
96
|
+
<div class="max-w-7xl mx-auto px-6 md:px-10 py-10">
|
|
97
|
+
<header class="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6">
|
|
98
|
+
<div>
|
|
99
|
+
<h1 class="text-5xl font-black text-slate-900 tracking-tight">User <span class="text-blue-600">Personas</span></h1>
|
|
100
|
+
<p class="text-lg text-slate-500 mt-2">Creez et gerez vos personas cibles.</p>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="flex items-center gap-4">
|
|
103
|
+
<div class="relative group">
|
|
104
|
+
<button class="px-5 py-4 bg-white border border-slate-200 rounded-2xl font-bold text-slate-700 flex items-center gap-3 hover:border-blue-300 transition-all shadow-sm">
|
|
105
|
+
<Icon :name="layoutIcon" class="text-xl text-blue-600" />
|
|
106
|
+
<span class="text-sm">{{ layoutLabel }}</span>
|
|
107
|
+
<Icon name="mdi:chevron-down" class="text-slate-300" />
|
|
108
|
+
</button>
|
|
109
|
+
<div class="absolute right-0 mt-3 w-48 bg-white border border-slate-100 rounded-2xl shadow-2xl py-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-30">
|
|
110
|
+
<button @click="currentLayout = 'table'" class="w-full px-4 py-3 text-left hover:bg-blue-50 flex items-center gap-3">
|
|
111
|
+
<Icon name="mdi:table" class="text-slate-400" />
|
|
112
|
+
<span :class="currentLayout === 'table' ? 'text-blue-600 font-black' : 'text-slate-600 font-bold'">Tableau</span>
|
|
113
|
+
</button>
|
|
114
|
+
<button @click="currentLayout = 'bento'" class="w-full px-4 py-3 text-left hover:bg-emerald-50 flex items-center gap-3">
|
|
115
|
+
<Icon name="mdi:view-dashboard-variant" class="text-slate-400" />
|
|
116
|
+
<span :class="currentLayout === 'bento' ? 'text-emerald-600 font-black' : 'text-slate-600 font-bold'">Grille</span>
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<button @click="openCreate" class="px-8 py-4 bg-slate-900 text-white rounded-2xl font-black text-sm uppercase hover:bg-blue-600 transition-all flex items-center gap-3 shadow-xl">
|
|
121
|
+
<Icon name="mdi:plus" class="text-xl" /> Nouveau
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</header>
|
|
125
|
+
|
|
126
|
+
<div v-if="loading" class="grid grid-cols-3 gap-6">
|
|
127
|
+
<div v-for="i in 3" :key="i" class="h-40 bg-slate-100 animate-pulse rounded-3xl"></div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div v-else-if="personas.length === 0" class="bg-white rounded-[3rem] p-20 text-center border-2 border-dashed border-slate-200">
|
|
131
|
+
<Icon name="mdi:account-question-outline" class="text-6xl text-slate-200 mb-6" />
|
|
132
|
+
<h3 class="text-2xl font-black text-slate-800">Aucune persona</h3>
|
|
133
|
+
<p class="text-slate-400 mt-2 mb-8">Creez vos premiers profils comportementaux.</p>
|
|
134
|
+
<button @click="openCreate" class="px-8 py-3 bg-blue-600 text-white rounded-2xl font-black text-sm uppercase hover:bg-slate-900 transition-all">Creer ma premiere persona</button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div v-else>
|
|
138
|
+
<!-- Table Layout -->
|
|
139
|
+
<div v-if="currentLayout === 'table'" class="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
|
140
|
+
<table class="w-full">
|
|
141
|
+
<thead>
|
|
142
|
+
<tr class="bg-slate-50 border-b border-slate-200">
|
|
143
|
+
<th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Avatar</th>
|
|
144
|
+
<th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Nom</th>
|
|
145
|
+
<th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Profession</th>
|
|
146
|
+
<th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Age</th>
|
|
147
|
+
<th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Ville</th>
|
|
148
|
+
<th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Besoins</th>
|
|
149
|
+
<th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Actions</th>
|
|
150
|
+
</tr>
|
|
151
|
+
</thead>
|
|
152
|
+
<tbody class="divide-y divide-slate-200">
|
|
153
|
+
<tr v-for="persona in personas" :key="persona.id" class="hover:bg-slate-50 transition-colors cursor-pointer" @click="viewPersona(persona)">
|
|
154
|
+
<td class="px-6 py-4">
|
|
155
|
+
<div class="w-12 h-12 rounded-xl overflow-hidden bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center text-white font-black">
|
|
156
|
+
<img v-if="persona.avatar_url" :src="persona.avatar_url" :alt="persona.name" class="w-full h-full object-cover" />
|
|
157
|
+
<span v-else>{{ getInitials(persona.name) }}</span>
|
|
158
|
+
</div>
|
|
159
|
+
</td>
|
|
160
|
+
<td class="px-6 py-4 font-bold text-slate-900">{{ persona.name }}</td>
|
|
161
|
+
<td class="px-6 py-4 text-sm text-slate-600">{{ persona.job_title || "-" }}</td>
|
|
162
|
+
<td class="px-6 py-4 text-sm text-slate-600">{{ persona.age || "-" }}</td>
|
|
163
|
+
<td class="px-6 py-4 text-sm text-slate-600">{{ persona.location || "-" }}</td>
|
|
164
|
+
<td class="px-6 py-4">
|
|
165
|
+
<div class="flex flex-wrap gap-1">
|
|
166
|
+
<span v-for="need in (persona.core_needs || []).slice(0, 2)" :key="need" class="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs font-semibold">{{ need }}</span>
|
|
167
|
+
<span v-if="(persona.core_needs || []).length > 2" class="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs font-semibold">+{{ persona.core_needs.length - 2 }}</span>
|
|
168
|
+
</div>
|
|
169
|
+
</td>
|
|
170
|
+
<td class="px-6 py-4" @click.stop>
|
|
171
|
+
<div class="flex items-center gap-2">
|
|
172
|
+
<button @click="viewPersona(persona)" class="p-2 hover:bg-blue-100 text-blue-600 rounded-lg"><Icon name="mdi:eye" class="text-lg" /></button>
|
|
173
|
+
<button @click="openEdit(persona)" class="p-2 hover:bg-purple-100 text-purple-600 rounded-lg"><Icon name="mdi:pencil" class="text-lg" /></button>
|
|
174
|
+
<button @click="handleDelete(persona.id)" class="p-2 hover:bg-red-100 text-red-600 rounded-lg"><Icon name="mdi:delete" class="text-lg" /></button>
|
|
175
|
+
</div>
|
|
176
|
+
</td>
|
|
177
|
+
</tr>
|
|
178
|
+
</tbody>
|
|
179
|
+
</table>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Bento Layout -->
|
|
183
|
+
<div v-else class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
184
|
+
<div v-for="persona in personas" :key="persona.id" class="bg-white rounded-[2rem] border border-slate-100 p-8 shadow-sm hover:shadow-xl hover:scale-[1.02] transition-all cursor-pointer group" @click="viewPersona(persona)">
|
|
185
|
+
<div class="flex items-center gap-4 mb-6">
|
|
186
|
+
<div class="w-16 h-16 rounded-2xl shrink-0 overflow-hidden bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center text-white font-black text-xl">
|
|
187
|
+
<img v-if="persona.avatar_url" :src="persona.avatar_url" class="w-full h-full object-cover" />
|
|
188
|
+
<span v-else>{{ getInitials(persona.name) }}</span>
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<h3 class="font-black text-lg text-slate-800 leading-none">{{ persona.name }}</h3>
|
|
192
|
+
<p class="text-[10px] font-black text-blue-600 uppercase tracking-widest mt-1">{{ persona.job_title }}</p>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="p-4 bg-slate-50 rounded-2xl">
|
|
196
|
+
<p class="text-xs font-black text-slate-400 uppercase mb-2">A propos</p>
|
|
197
|
+
<p class="text-sm font-bold text-slate-700 line-clamp-2">"{{ persona.about || "Aucune description..." }}"</p>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="flex items-center justify-between text-[10px] font-black uppercase text-slate-400 px-1 mt-4">
|
|
200
|
+
<span class="flex items-center gap-1"><Icon name="mdi:map-marker" /> {{ persona.location || "-" }}</span>
|
|
201
|
+
<span class="text-blue-600 group-hover:translate-x-1 transition-transform">Details</span>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Modal -->
|
|
209
|
+
<Teleport to="body">
|
|
210
|
+
<div v-if="showModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="closeModal">
|
|
211
|
+
<div class="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl">
|
|
212
|
+
<div class="p-8">
|
|
213
|
+
<div class="flex items-center justify-between mb-6">
|
|
214
|
+
<h2 class="text-2xl font-black text-slate-900">{{ editingPersona ? "Modifier Persona" : "Nouvelle Persona" }}</h2>
|
|
215
|
+
<button @click="closeModal" class="p-2 hover:bg-slate-100 rounded-xl"><Icon name="mdi:close" class="text-xl text-slate-400" /></button>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="space-y-5">
|
|
218
|
+
<div class="grid grid-cols-2 gap-4">
|
|
219
|
+
<div>
|
|
220
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Nom *</label>
|
|
221
|
+
<input v-model="formData.name" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none font-bold" />
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Profession</label>
|
|
225
|
+
<input v-model="formData.job_title" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
|
|
226
|
+
</div>
|
|
227
|
+
<div>
|
|
228
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Age</label>
|
|
229
|
+
<input v-model.number="formData.age" type="number" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
|
|
230
|
+
</div>
|
|
231
|
+
<div>
|
|
232
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Ville</label>
|
|
233
|
+
<input v-model="formData.location" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<div>
|
|
237
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">A propos</label>
|
|
238
|
+
<textarea v-model="formData.about" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none"></textarea>
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
241
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">URL Avatar</label>
|
|
242
|
+
<input v-model="formData.avatar_url" type="url" placeholder="https://..." class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
|
|
243
|
+
</div>
|
|
244
|
+
<div>
|
|
245
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Besoins principaux (un par ligne)</label>
|
|
246
|
+
<textarea v-model="coreNeedsText" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none text-sm"></textarea>
|
|
247
|
+
</div>
|
|
248
|
+
<div>
|
|
249
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Objectifs (un par ligne)</label>
|
|
250
|
+
<textarea v-model="goalsText" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none text-sm"></textarea>
|
|
251
|
+
</div>
|
|
252
|
+
<div>
|
|
253
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Points de douleur (un par ligne)</label>
|
|
254
|
+
<textarea v-model="painPointsText" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none text-sm"></textarea>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="flex gap-3 pt-4 border-t">
|
|
257
|
+
<button @click="savePersona" :disabled="saving" class="flex-1 px-6 py-3 bg-blue-600 text-white font-black rounded-xl hover:bg-blue-700 disabled:opacity-50">{{ saving ? "Enregistrement..." : "Enregistrer" }}</button>
|
|
258
|
+
<button @click="closeModal" class="px-6 py-3 bg-slate-100 text-slate-700 font-bold rounded-xl hover:bg-slate-200">Annuler</button>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</Teleport>
|
|
265
|
+
</AdminDashboardLayout>
|
|
266
|
+
</template>
|