Initial commit — Diabetix V2
Application Laravel 12 + Inertia + Vue 3 + Tailwind. Fonctionnalités : dashboard glycémique, saisie de mesures, courbe SVG, statistiques (jour/semaine/mois/trimestre), défis & badges, chat coach IA (Gemini), paramètres profil avec palette de couleurs, pages auth redessinées, emails transactionnels via Resend avec thème Diabetix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
108
app/resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
108
app/resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import DangerButton from '@/Components/DangerButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import Modal from '@/Components/Modal.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
const confirmingUserDeletion = ref(false);
|
||||
const passwordInput = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const confirmUserDeletion = () => {
|
||||
confirmingUserDeletion.value = true;
|
||||
|
||||
nextTick(() => passwordInput.value.focus());
|
||||
};
|
||||
|
||||
const deleteUser = () => {
|
||||
form.delete(route('profile.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeModal(),
|
||||
onError: () => passwordInput.value.focus(),
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
confirmingUserDeletion.value = false;
|
||||
|
||||
form.clearErrors();
|
||||
form.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Delete Account
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Once your account is deleted, all of its resources and data will
|
||||
be permanently deleted. Before deleting your account, please
|
||||
download any data or information that you wish to retain.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<DangerButton @click="confirmUserDeletion">Delete Account</DangerButton>
|
||||
|
||||
<Modal :show="confirmingUserDeletion" @close="closeModal">
|
||||
<div class="p-6">
|
||||
<h2
|
||||
class="text-lg font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Are you sure you want to delete your account?
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Once your account is deleted, all of its resources and data
|
||||
will be permanently deleted. Please enter your password to
|
||||
confirm you would like to permanently delete your account.
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<InputLabel
|
||||
for="password"
|
||||
value="Password"
|
||||
class="sr-only"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Password"
|
||||
@keyup.enter="deleteUser"
|
||||
/>
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<SecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="deleteUser"
|
||||
>
|
||||
Delete Account
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Link, useForm, usePage } from '@inertiajs/vue3';
|
||||
|
||||
const user = usePage().props.auth.user;
|
||||
|
||||
const form = useForm({
|
||||
first_name: user.first_name ?? '',
|
||||
diabetes_type: user.diabetes_type ?? 'type2',
|
||||
target_min: user.target_min ?? 70,
|
||||
target_max: user.target_max ?? 180,
|
||||
palette: user.palette ?? 'mint',
|
||||
});
|
||||
|
||||
const palettes = [
|
||||
{ id: 'mint', label: 'Menthe', swatch: '#7bbfb5' },
|
||||
{ id: 'lilac', label: 'Lilas', swatch: '#9b8ec4' },
|
||||
{ id: 'peach', label: 'Pêche', swatch: '#d4826a' },
|
||||
];
|
||||
|
||||
const types = [
|
||||
{ id: 'type1', label: 'Type 1' },
|
||||
{ id: 'type2', label: 'Type 2' },
|
||||
{ id: 'gestational', label: 'Gestationnel' },
|
||||
{ id: 'other', label: 'Autre' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Préférences Diabetix</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Personnalisez votre prénom, votre type de diabète, votre plage cible (mg/dL) et le thème de l'application.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="form.patch(route('profile.diabetix'), { preserveScroll: true })" class="mt-6 space-y-6">
|
||||
<div>
|
||||
<InputLabel for="first_name" value="Prénom" />
|
||||
<TextInput id="first_name" v-model="form.first_name" type="text" class="mt-1 block w-full" autocomplete="given-name" />
|
||||
<InputError :message="form.errors.first_name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel value="Type de diabète" />
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button v-for="t in types" :key="t.id" type="button" @click="form.diabetes_type = t.id"
|
||||
:class="[
|
||||
'rounded-full border px-4 py-1.5 text-sm transition',
|
||||
form.diabetes_type === t.id
|
||||
? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-200'
|
||||
: 'border-gray-300 text-gray-600 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700',
|
||||
]">{{ t.label }}</button>
|
||||
</div>
|
||||
<InputError :message="form.errors.diabetes_type" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel for="target_min" value="Cible min (mg/dL)" />
|
||||
<TextInput id="target_min" v-model.number="form.target_min" type="number" min="50" max="140" class="mt-1 block w-full" />
|
||||
<InputError :message="form.errors.target_min" class="mt-2" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="target_max" value="Cible max (mg/dL)" />
|
||||
<TextInput id="target_max" v-model.number="form.target_max" type="number" min="120" max="300" class="mt-1 block w-full" />
|
||||
<InputError :message="form.errors.target_max" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Valeurs par défaut OMS / consensus international : 70–180 mg/dL.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<InputLabel value="Thème de couleur" />
|
||||
<div class="mt-2 flex gap-3">
|
||||
<button v-for="p in palettes" :key="p.id" type="button" @click="form.palette = p.id"
|
||||
:class="[
|
||||
'flex items-center gap-2 rounded-full border px-4 py-2 text-sm transition',
|
||||
form.palette === p.id
|
||||
? 'border-gray-900 dark:border-gray-100'
|
||||
: 'border-gray-300 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700',
|
||||
]">
|
||||
<span class="block h-4 w-4 rounded-full" :style="{ background: p.swatch }" />
|
||||
<span>{{ p.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<InputError :message="form.errors.palette" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</PrimaryButton>
|
||||
<Transition enter-from-class="opacity-0" leave-to-class="opacity-0" class="transition ease-in-out">
|
||||
<p v-if="form.recentlySuccessful" class="text-sm text-gray-600 dark:text-gray-400">Préférences enregistrées.</p>
|
||||
</Transition>
|
||||
<Link :href="route('dashboard')" class="ml-auto text-sm text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100">← Retour à l'app</Link>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
122
app/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
122
app/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const passwordInput = ref(null);
|
||||
const currentPasswordInput = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const updatePassword = () => {
|
||||
form.put(route('password.update'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => form.reset(),
|
||||
onError: () => {
|
||||
if (form.errors.password) {
|
||||
form.reset('password', 'password_confirmation');
|
||||
passwordInput.value.focus();
|
||||
}
|
||||
if (form.errors.current_password) {
|
||||
form.reset('current_password');
|
||||
currentPasswordInput.value.focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Update Password
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Ensure your account is using a long, random password to stay
|
||||
secure.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="updatePassword" class="mt-6 space-y-6">
|
||||
<div>
|
||||
<InputLabel for="current_password" value="Current Password" />
|
||||
|
||||
<TextInput
|
||||
id="current_password"
|
||||
ref="currentPasswordInput"
|
||||
v-model="form.current_password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<InputError
|
||||
:message="form.errors.current_password"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="password" value="New Password" />
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirm Password"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError
|
||||
:message="form.errors.password_confirmation"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Save</PrimaryButton>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-if="form.recentlySuccessful"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Saved.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Link, useForm, usePage } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
mustVerifyEmail: {
|
||||
type: Boolean,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const user = usePage().props.auth.user;
|
||||
|
||||
const form = useForm({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Profile Information
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Update your account's profile information and email address.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
@submit.prevent="form.patch(route('profile.update'))"
|
||||
class="mt-6 space-y-6"
|
||||
>
|
||||
<div>
|
||||
<InputLabel for="name" value="Name" />
|
||||
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div v-if="mustVerifyEmail && user.email_verified_at === null">
|
||||
<p class="mt-2 text-sm text-gray-800 dark:text-gray-200">
|
||||
Your email address is unverified.
|
||||
<Link
|
||||
:href="route('verification.send')"
|
||||
method="post"
|
||||
as="button"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
Click here to re-send the verification email.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-show="status === 'verification-link-sent'"
|
||||
class="mt-2 text-sm font-medium text-green-600 dark:text-green-400"
|
||||
>
|
||||
A new verification link has been sent to your email address.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Save</PrimaryButton>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-if="form.recentlySuccessful"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Saved.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user