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:
55
app/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
55
app/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
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 { Head, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.confirm'), {
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Confirm Password" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
This is a secure area of the application. Please confirm your
|
||||
password before continuing.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="password" value="Password" />
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
autofocus
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<PrimaryButton
|
||||
class="ms-4"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Confirm
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
60
app/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
60
app/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import { tokens, FONT } from '@/Components/Diabetix/palette.js';
|
||||
|
||||
defineProps({ status: String });
|
||||
|
||||
const tok = tokens('mint');
|
||||
const font = FONT;
|
||||
|
||||
const form = useForm({ email: '' });
|
||||
|
||||
function submit() {
|
||||
form.post(route('password.email'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Mot de passe oublié" />
|
||||
|
||||
<div :style="{ minHeight: '100vh', background: '#eeece8', fontFamily: font.body, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }">
|
||||
<div :style="{ width: '100%', maxWidth: '400px' }">
|
||||
|
||||
<div style="text-align:center;margin-bottom:32px;">
|
||||
<div :style="{ width: '64px', height: '64px', borderRadius: '50%', background: tok.light, border: '2px solid ' + tok.primary, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '28px', margin: '0 auto 14px' }">🔑</div>
|
||||
<div :style="{ fontFamily: font.title, fontSize: '24px', fontWeight: 700, color: tok.text }">Mot de passe oublié</div>
|
||||
<div :style="{ fontSize: '13px', color: tok.muted, marginTop: '6px', lineHeight: 1.5 }">Saisissez votre e-mail — nous vous enverrons un lien pour réinitialiser votre mot de passe.</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ background: tok.white, borderRadius: '24px', padding: '28px 24px', boxShadow: '0 4px 24px rgba(42,53,51,0.10)' }">
|
||||
<div v-if="status" :style="{ background: tok.light, borderRadius: '12px', padding: '10px 14px', fontSize: '13px', color: tok.dark, marginBottom: '20px' }">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div style="margin-bottom:24px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Adresse e-mail</label>
|
||||
<input v-model="form.email" type="email" required autofocus autocomplete="username"
|
||||
:style="{ width: '100%', padding: '12px 14px', borderRadius: '14px', border: '1.5px solid ' + tok.border, background: tok.bg, fontSize: '15px', color: tok.text, outline: 'none', boxSizing: 'border-box' }" />
|
||||
<span v-if="form.errors.email" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.email }}</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="form.processing"
|
||||
:style="{
|
||||
width: '100%', padding: '15px', background: tok.primary, color: '#fff',
|
||||
borderRadius: '16px', border: 'none', fontSize: '15px', fontWeight: 700,
|
||||
cursor: form.processing ? 'not-allowed' : 'pointer',
|
||||
opacity: form.processing ? 0.6 : 1,
|
||||
boxShadow: '0 6px 18px rgba(123,191,181,0.35)',
|
||||
}">
|
||||
{{ form.processing ? 'Envoi…' : 'Envoyer le lien' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;margin-top:20px;">
|
||||
<Link :href="route('login')" :style="{ fontSize: '13px', color: tok.primary, fontWeight: 600, textDecoration: 'none' }">← Retour à la connexion</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
110
app/resources/js/Pages/Auth/Login.vue
Normal file
110
app/resources/js/Pages/Auth/Login.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import { tokens, FONT } from '@/Components/Diabetix/palette.js';
|
||||
|
||||
defineProps({
|
||||
canResetPassword: Boolean,
|
||||
status: String,
|
||||
});
|
||||
|
||||
const tok = tokens('mint');
|
||||
const font = FONT;
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.post(route('login'), {
|
||||
onFinish: () => form.reset('password'),
|
||||
});
|
||||
}
|
||||
|
||||
function inputStyle() {
|
||||
return {
|
||||
width: '100%', padding: '12px 14px', borderRadius: '14px',
|
||||
border: '1.5px solid ' + tok.border, background: tok.bg,
|
||||
fontSize: '15px', color: tok.text, outline: 'none', boxSizing: 'border-box',
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Connexion" />
|
||||
|
||||
<div :style="{ minHeight: '100vh', background: '#eeece8', fontFamily: font.body, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }">
|
||||
<div :style="{ width: '100%', maxWidth: '400px' }">
|
||||
|
||||
<!-- Logo / titre -->
|
||||
<div style="text-align:center;margin-bottom:32px;">
|
||||
<div :style="{ width: '64px', height: '64px', borderRadius: '50%', background: tok.light, border: '2px solid ' + tok.primary, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '28px', margin: '0 auto 14px' }">🩺</div>
|
||||
<div :style="{ fontFamily: font.title, fontSize: '26px', fontWeight: 700, color: tok.text }">Diabetix</div>
|
||||
<div :style="{ fontSize: '13px', color: tok.muted, marginTop: '4px' }">Votre coach glycémique personnel</div>
|
||||
</div>
|
||||
|
||||
<!-- Carte -->
|
||||
<div :style="{ background: tok.white, borderRadius: '24px', padding: '28px 24px', boxShadow: '0 4px 24px rgba(42,53,51,0.10)' }">
|
||||
|
||||
<div v-if="status" :style="{ background: tok.light, borderRadius: '12px', padding: '10px 14px', fontSize: '13px', color: tok.dark, marginBottom: '20px' }">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div style="margin-bottom:16px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Adresse e-mail</label>
|
||||
<input v-model="form.email" type="email" required autofocus autocomplete="username"
|
||||
:style="inputStyle()"
|
||||
:class="form.errors.email ? 'border-red-400' : ''" />
|
||||
<span v-if="form.errors.email" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.email }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.7px' }">Mot de passe</label>
|
||||
<Link v-if="canResetPassword" :href="route('password.request')"
|
||||
:style="{ fontSize: '11px', color: tok.primary, textDecoration: 'none' }">
|
||||
Mot de passe oublié ?
|
||||
</Link>
|
||||
</div>
|
||||
<input v-model="form.password" type="password" required autocomplete="current-password"
|
||||
:style="inputStyle()" />
|
||||
<span v-if="form.errors.password" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.password }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Se souvenir -->
|
||||
<label style="display:flex;align-items:center;gap:10px;margin-bottom:24px;cursor:pointer;">
|
||||
<span :style="{
|
||||
width: '20px', height: '20px', borderRadius: '6px', flexShrink: 0,
|
||||
border: '1.5px solid ' + (form.remember ? tok.primary : tok.border),
|
||||
background: form.remember ? tok.primary : tok.white,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}" @click="form.remember = !form.remember">
|
||||
<span v-if="form.remember" style="color:#fff;font-size:12px;font-weight:700;">✓</span>
|
||||
</span>
|
||||
<span :style="{ fontSize: '13px', color: tok.muted }">Se souvenir de moi</span>
|
||||
</label>
|
||||
|
||||
<button type="submit" :disabled="form.processing"
|
||||
:style="{
|
||||
width: '100%', padding: '15px', background: tok.primary, color: '#fff',
|
||||
borderRadius: '16px', border: 'none', fontSize: '15px', fontWeight: 700,
|
||||
cursor: form.processing ? 'not-allowed' : 'pointer',
|
||||
opacity: form.processing ? 0.6 : 1,
|
||||
boxShadow: '0 6px 18px rgba(123,191,181,0.35)',
|
||||
}">
|
||||
{{ form.processing ? 'Connexion…' : 'Se connecter' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Lien inscription -->
|
||||
<div style="text-align:center;margin-top:20px;">
|
||||
<span :style="{ fontSize: '13px', color: tok.muted }">Pas encore de compte ? </span>
|
||||
<Link :href="route('register')" :style="{ fontSize: '13px', color: tok.primary, fontWeight: 600, textDecoration: 'none' }">Créer un compte</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
91
app/resources/js/Pages/Auth/Register.vue
Normal file
91
app/resources/js/Pages/Auth/Register.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import { tokens, FONT } from '@/Components/Diabetix/palette.js';
|
||||
|
||||
const tok = tokens('mint');
|
||||
const font = FONT;
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.post(route('register'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
}
|
||||
|
||||
function inputStyle() {
|
||||
return {
|
||||
width: '100%', padding: '12px 14px', borderRadius: '14px',
|
||||
border: '1.5px solid ' + tok.border, background: tok.bg,
|
||||
fontSize: '15px', color: tok.text, outline: 'none', boxSizing: 'border-box',
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Créer un compte" />
|
||||
|
||||
<div :style="{ minHeight: '100vh', background: '#eeece8', fontFamily: font.body, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }">
|
||||
<div :style="{ width: '100%', maxWidth: '400px' }">
|
||||
|
||||
<!-- Logo -->
|
||||
<div style="text-align:center;margin-bottom:32px;">
|
||||
<div :style="{ width: '64px', height: '64px', borderRadius: '50%', background: tok.light, border: '2px solid ' + tok.primary, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '28px', margin: '0 auto 14px' }">🩺</div>
|
||||
<div :style="{ fontFamily: font.title, fontSize: '26px', fontWeight: 700, color: tok.text }">Diabetix</div>
|
||||
<div :style="{ fontSize: '13px', color: tok.muted, marginTop: '4px' }">Créez votre compte gratuitement</div>
|
||||
</div>
|
||||
|
||||
<!-- Carte -->
|
||||
<div :style="{ background: tok.white, borderRadius: '24px', padding: '28px 24px', boxShadow: '0 4px 24px rgba(42,53,51,0.10)' }">
|
||||
<form @submit.prevent="submit">
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Nom complet</label>
|
||||
<input v-model="form.name" type="text" required autofocus autocomplete="name" :style="inputStyle()" />
|
||||
<span v-if="form.errors.name" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.name }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Adresse e-mail</label>
|
||||
<input v-model="form.email" type="email" required autocomplete="username" :style="inputStyle()" />
|
||||
<span v-if="form.errors.email" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.email }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Mot de passe</label>
|
||||
<input v-model="form.password" type="password" required autocomplete="new-password" :style="inputStyle()" />
|
||||
<span v-if="form.errors.password" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.password }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:24px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Confirmer le mot de passe</label>
|
||||
<input v-model="form.password_confirmation" type="password" required autocomplete="new-password" :style="inputStyle()" />
|
||||
<span v-if="form.errors.password_confirmation" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.password_confirmation }}</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="form.processing"
|
||||
:style="{
|
||||
width: '100%', padding: '15px', background: tok.primary, color: '#fff',
|
||||
borderRadius: '16px', border: 'none', fontSize: '15px', fontWeight: 700,
|
||||
cursor: form.processing ? 'not-allowed' : 'pointer',
|
||||
opacity: form.processing ? 0.6 : 1,
|
||||
boxShadow: '0 6px 18px rgba(123,191,181,0.35)',
|
||||
}">
|
||||
{{ form.processing ? 'Création…' : 'Créer mon compte' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Lien connexion -->
|
||||
<div style="text-align:center;margin-top:20px;">
|
||||
<span :style="{ fontSize: '13px', color: tok.muted }">Déjà un compte ? </span>
|
||||
<Link :href="route('login')" :style="{ fontSize: '13px', color: tok.primary, fontWeight: 600, textDecoration: 'none' }">Se connecter</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
82
app/resources/js/Pages/Auth/ResetPassword.vue
Normal file
82
app/resources/js/Pages/Auth/ResetPassword.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { tokens, FONT } from '@/Components/Diabetix/palette.js';
|
||||
|
||||
const props = defineProps({
|
||||
email: { type: String, required: true },
|
||||
token: { type: String, required: true },
|
||||
});
|
||||
|
||||
const tok = tokens('mint');
|
||||
const font = FONT;
|
||||
|
||||
const form = useForm({
|
||||
token: props.token,
|
||||
email: props.email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.post(route('password.store'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
}
|
||||
|
||||
function inputStyle() {
|
||||
return {
|
||||
width: '100%', padding: '12px 14px', borderRadius: '14px',
|
||||
border: '1.5px solid ' + tok.border, background: tok.bg,
|
||||
fontSize: '15px', color: tok.text, outline: 'none', boxSizing: 'border-box',
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Réinitialiser le mot de passe" />
|
||||
|
||||
<div :style="{ minHeight: '100vh', background: '#eeece8', fontFamily: font.body, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }">
|
||||
<div :style="{ width: '100%', maxWidth: '400px' }">
|
||||
|
||||
<div style="text-align:center;margin-bottom:32px;">
|
||||
<div :style="{ width: '64px', height: '64px', borderRadius: '50%', background: tok.light, border: '2px solid ' + tok.primary, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '28px', margin: '0 auto 14px' }">🔒</div>
|
||||
<div :style="{ fontFamily: font.title, fontSize: '24px', fontWeight: 700, color: tok.text }">Nouveau mot de passe</div>
|
||||
<div :style="{ fontSize: '13px', color: tok.muted, marginTop: '4px' }">Choisissez un mot de passe sécurisé.</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ background: tok.white, borderRadius: '24px', padding: '28px 24px', boxShadow: '0 4px 24px rgba(42,53,51,0.10)' }">
|
||||
<form @submit.prevent="submit">
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Adresse e-mail</label>
|
||||
<input v-model="form.email" type="email" required autocomplete="username" :style="inputStyle()" />
|
||||
<span v-if="form.errors.email" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.email }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Nouveau mot de passe</label>
|
||||
<input v-model="form.password" type="password" required autofocus autocomplete="new-password" :style="inputStyle()" />
|
||||
<span v-if="form.errors.password" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.password }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:24px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.7px' }">Confirmer le mot de passe</label>
|
||||
<input v-model="form.password_confirmation" type="password" required autocomplete="new-password" :style="inputStyle()" />
|
||||
<span v-if="form.errors.password_confirmation" :style="{ fontSize: '11px', color: '#c43', marginTop: '4px', display: 'block' }">{{ form.errors.password_confirmation }}</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="form.processing"
|
||||
:style="{
|
||||
width: '100%', padding: '15px', background: tok.primary, color: '#fff',
|
||||
borderRadius: '16px', border: 'none', fontSize: '15px', fontWeight: 700,
|
||||
cursor: form.processing ? 'not-allowed' : 'pointer',
|
||||
opacity: form.processing ? 0.6 : 1,
|
||||
boxShadow: '0 6px 18px rgba(123,191,181,0.35)',
|
||||
}">
|
||||
{{ form.processing ? 'Enregistrement…' : 'Réinitialiser le mot de passe' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
61
app/resources/js/Pages/Auth/VerifyEmail.vue
Normal file
61
app/resources/js/Pages/Auth/VerifyEmail.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('verification.send'));
|
||||
};
|
||||
|
||||
const verificationLinkSent = computed(
|
||||
() => props.status === 'verification-link-sent',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Email Verification" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Thanks for signing up! Before getting started, could you verify your
|
||||
email address by clicking on the link we just emailed to you? If you
|
||||
didn't receive the email, we will gladly send you another.
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-4 text-sm font-medium text-green-600 dark:text-green-400"
|
||||
v-if="verificationLinkSent"
|
||||
>
|
||||
A new verification link has been sent to the email address you
|
||||
provided during registration.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Resend Verification Email
|
||||
</PrimaryButton>
|
||||
|
||||
<Link
|
||||
:href="route('logout')"
|
||||
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"
|
||||
>Log Out</Link
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
30
app/resources/js/Pages/Dashboard.vue
Normal file
30
app/resources/js/Pages/Dashboard.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2
|
||||
class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
Dashboard
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800"
|
||||
>
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
You're logged in!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
117
app/resources/js/Pages/Diabetix/Challenges.vue
Normal file
117
app/resources/js/Pages/Diabetix/Challenges.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import DiabetixLayout from '@/Layouts/DiabetixLayout.vue';
|
||||
import ProgressBar from '@/Components/Diabetix/ProgressBar.vue';
|
||||
import BadgeChip from '@/Components/Diabetix/BadgeChip.vue';
|
||||
|
||||
const props = defineProps({
|
||||
challenges: Array,
|
||||
badges: Array,
|
||||
level: Object,
|
||||
});
|
||||
|
||||
const tab = ref('defis');
|
||||
|
||||
const unlocked = computed(() => props.badges.filter(b => b.unlocked));
|
||||
const locked = computed(() => props.badges.filter(b => !b.unlocked));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Défis & Badges" />
|
||||
<DiabetixLayout v-slot="{ tok, font }">
|
||||
<div :style="{ background: tok.white, padding: '14px 20px 18px', borderBottom: '1px solid ' + tok.border }">
|
||||
<div :style="{ fontFamily: font.title, fontSize: '20px', fontWeight: 600, color: tok.text, marginBottom: '14px' }">Défis & Badges</div>
|
||||
|
||||
<div :style="{ background: tok.light, borderRadius: '18px', padding: '16px' }">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div>
|
||||
<div :style="{ fontSize: '11px', color: tok.muted }">Niveau actuel</div>
|
||||
<div :style="{ fontFamily: font.title, fontSize: '17px', fontWeight: 600, color: tok.text }">{{ level.icon }} {{ level.label }}</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div :style="{ fontSize: '11px', color: tok.muted }">Points</div>
|
||||
<div :style="{ fontSize: '22px', fontWeight: 800, color: tok.amber }">{{ level.points.toLocaleString('fr-FR') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBar :value="level.points" :max="level.next_at" :color="tok.amber" :tok="tok" :height="8" />
|
||||
<div style="display:flex;justify-content:space-between;margin-top:5px;">
|
||||
<span :style="{ fontSize: '10px', color: tok.muted }">{{ level.icon }} {{ level.label }}</span>
|
||||
<span :style="{ fontSize: '10px', color: tok.amber }">{{ level.remaining }} pts → {{ level.next_icon }} {{ level.next_label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px 20px 0;">
|
||||
<div :style="{ display: 'flex', gap: '4px', background: tok.bgAlt, borderRadius: '12px', padding: '4px' }">
|
||||
<button v-for="t in [['defis','🎯 Défis actifs'],['badges','🏅 Badges']]" :key="t[0]"
|
||||
@click="tab = t[0]"
|
||||
:style="{
|
||||
flex: 1, padding: '8px', borderRadius: '9px', border: 'none',
|
||||
background: tab === t[0] ? tok.primary : 'transparent',
|
||||
color: tab === t[0] ? '#fff' : tok.muted,
|
||||
fontSize: '12px', fontWeight: tab === t[0] ? 600 : 400, cursor: 'pointer',
|
||||
}">{{ t[1] }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px 20px;display:flex;flex-direction:column;gap:14px;">
|
||||
<template v-if="tab === 'defis'">
|
||||
<div v-for="c in challenges" :key="c.id"
|
||||
:style="{ background: tok.white, borderRadius: '18px', padding: '16px', boxShadow: '0 2px 12px rgba(42,53,51,0.06)' }">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;">
|
||||
<div style="font-size:28px;">{{ c.icon }}</div>
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div :style="{ fontSize: '14px', fontWeight: 600, color: tok.text }">{{ c.title }}</div>
|
||||
<div :style="{ background: tok.amberLight, borderRadius: '20px', padding: '3px 8px', fontSize: '10px', color: tok.amber, fontWeight: 600 }">+{{ c.points }} pts</div>
|
||||
</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginBottom: '10px' }">{{ c.description }}</div>
|
||||
<ProgressBar :value="c.progress" :tok="tok" :height="7" />
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:5px;">
|
||||
<span :style="{ fontSize: '11px', color: tok.primary }">{{ c.progress }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-if="unlocked.length">
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, fontWeight: 700, letterSpacing: '0.8px' }">DÉBLOQUÉS ({{ unlocked.length }})</div>
|
||||
<div v-for="b in unlocked" :key="b.id"
|
||||
:style="{ background: tok.white, borderRadius: '16px', padding: '14px 16px', boxShadow: '0 2px 10px rgba(42,53,51,0.06)', display: 'flex', alignItems: 'center', gap: '14px' }">
|
||||
<div :style="{
|
||||
width: '48px', height: '48px', borderRadius: '50%', flexShrink: 0,
|
||||
background: tok.light, border: '2px solid ' + tok.primary,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '22px',
|
||||
}">{{ b.icon }}</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 700, color: tok.text }">{{ b.label }}</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginTop: '2px', lineHeight: 1.4 }">{{ b.criteria }}</div>
|
||||
</div>
|
||||
<div :style="{ fontSize: '16px', flexShrink: 0 }">✅</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="locked.length">
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, fontWeight: 700, letterSpacing: '0.8px', marginTop: unlocked.length ? '4px' : 0 }">À DÉBLOQUER ({{ locked.length }})</div>
|
||||
<div v-for="b in locked" :key="b.id"
|
||||
:style="{ background: tok.white, borderRadius: '16px', padding: '14px 16px', boxShadow: '0 2px 10px rgba(42,53,51,0.04)', display: 'flex', alignItems: 'center', gap: '14px', opacity: 0.7 }">
|
||||
<div :style="{
|
||||
width: '48px', height: '48px', borderRadius: '50%', flexShrink: 0,
|
||||
background: tok.bgAlt, border: '2px solid ' + tok.border,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '22px',
|
||||
filter: 'grayscale(1)',
|
||||
}">{{ b.icon }}</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 600, color: tok.muted }">{{ b.label }}</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginTop: '2px', lineHeight: 1.4 }">{{ b.criteria }}</div>
|
||||
</div>
|
||||
<div :style="{ fontSize: '16px', flexShrink: 0 }">🔒</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</DiabetixLayout>
|
||||
</template>
|
||||
73
app/resources/js/Pages/Diabetix/Chat.vue
Normal file
73
app/resources/js/Pages/Diabetix/Chat.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import DiabetixLayout from '@/Layouts/DiabetixLayout.vue';
|
||||
import CoachAvatar from '@/Components/Diabetix/CoachAvatar.vue';
|
||||
|
||||
const props = defineProps({
|
||||
messages: Array,
|
||||
});
|
||||
|
||||
const form = useForm({ body: '' });
|
||||
const listRef = ref(null);
|
||||
|
||||
function send() {
|
||||
if (!form.body.trim()) return;
|
||||
form.post(route('chat.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => form.reset('body'),
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.messages.length, async () => {
|
||||
await nextTick();
|
||||
if (listRef.value) listRef.value.scrollTop = listRef.value.scrollHeight;
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Coach IA" />
|
||||
<DiabetixLayout v-slot="{ tok, font }">
|
||||
<div :style="{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 78px)', background: tok.bg }">
|
||||
<div :style="{ background: tok.white, padding: '12px 16px', display: 'flex', alignItems: 'center', gap: '10px', borderBottom: '1px solid ' + tok.border, flexShrink: 0 }">
|
||||
<CoachAvatar :size="36" :tok="tok" />
|
||||
<div>
|
||||
<div :style="{ fontFamily: font.title, fontSize: '16px', fontWeight: 600, color: tok.text }">Coach IA</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.primary }">🟢 En ligne · Répond en quelques secondes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="listRef" style="flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;">
|
||||
<div v-for="(msg, i) in messages" :key="msg.id ?? i"
|
||||
:style="{ display: 'flex', flexDirection: 'column', alignItems: msg.sender === 'coach' ? 'flex-start' : 'flex-end', gap: '5px' }">
|
||||
<div :style="{ display: 'flex', alignItems: 'flex-end', gap: '8px', flexDirection: msg.sender === 'coach' ? 'row' : 'row-reverse' }">
|
||||
<CoachAvatar v-if="msg.sender === 'coach'" :size="28" :tok="tok" />
|
||||
<div :style="{
|
||||
maxWidth: '78%',
|
||||
background: msg.sender === 'coach' ? tok.light : tok.primary,
|
||||
color: msg.sender === 'coach' ? tok.text : '#fff',
|
||||
borderRadius: msg.sender === 'coach' ? '14px 14px 14px 3px' : '14px 14px 3px 14px',
|
||||
padding: '10px 14px', fontSize: '13px', lineHeight: 1.5,
|
||||
}">{{ msg.body }}</div>
|
||||
</div>
|
||||
<div v-if="msg.actions && msg.actions.length" :style="{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginLeft: msg.sender === 'coach' ? '36px' : 0 }">
|
||||
<button v-for="(a, j) in msg.actions" :key="j" :style="{
|
||||
padding: '5px 13px', borderRadius: '20px', background: tok.white,
|
||||
border: '1.5px solid ' + tok.primary, fontSize: '11px',
|
||||
color: tok.primary, fontWeight: 500, cursor: 'pointer',
|
||||
}">{{ a }}</button>
|
||||
</div>
|
||||
<div :style="{ fontSize: '9px', color: tok.muted, paddingLeft: msg.sender === 'coach' ? '36px' : 0 }">{{ msg.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ padding: '10px 14px', background: tok.white, borderTop: '1px solid ' + tok.border, flexShrink: 0, display: 'flex', gap: '8px', alignItems: 'center' }">
|
||||
<input v-model="form.body" @keydown.enter="send"
|
||||
placeholder="Écrire au coach..."
|
||||
:style="{ flex: 1, padding: '10px 14px', borderRadius: '20px', border: '1.5px solid ' + tok.border, background: tok.bg, fontSize: '13px', color: tok.text, outline: 'none' }" />
|
||||
<button @click="send" :disabled="form.processing"
|
||||
:style="{ width: '38px', height: '38px', borderRadius: '50%', background: tok.primary, border: 'none', color: '#fff', fontSize: '17px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: form.processing ? 0.6 : 1 }">↑</button>
|
||||
</div>
|
||||
</div>
|
||||
</DiabetixLayout>
|
||||
</template>
|
||||
128
app/resources/js/Pages/Diabetix/Home.vue
Normal file
128
app/resources/js/Pages/Diabetix/Home.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Head, Link, usePage } from '@inertiajs/vue3';
|
||||
import DiabetixLayout from '@/Layouts/DiabetixLayout.vue';
|
||||
import ProgressBar from '@/Components/Diabetix/ProgressBar.vue';
|
||||
import CoachAvatar from '@/Components/Diabetix/CoachAvatar.vue';
|
||||
import BadgeChip from '@/Components/Diabetix/BadgeChip.vue';
|
||||
import GlucoseChart from '@/Components/Diabetix/GlucoseChart.vue';
|
||||
|
||||
const props = defineProps({
|
||||
metrics: Object,
|
||||
day_chart: Array,
|
||||
challenges: Array,
|
||||
badges: Array,
|
||||
});
|
||||
|
||||
const page = usePage();
|
||||
const userName = computed(() => page.props.auth.user?.first_name ?? 'ami');
|
||||
|
||||
const latest = computed(() => props.metrics.latest);
|
||||
const inRange = computed(() => latest.value?.in_range);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Accueil" />
|
||||
<DiabetixLayout v-slot="{ tok, font, openInput }">
|
||||
<!-- Header -->
|
||||
<div :style="{ background: tok.white, padding: '14px 20px 18px', borderBottom: '1px solid ' + tok.border }">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
|
||||
<CoachAvatar :size="42" :tok="tok" />
|
||||
<div style="flex:1;">
|
||||
<div :style="{ fontFamily: font.title, fontSize: '19px', color: tok.text, fontWeight: 600 }">
|
||||
Bonjour <Link :href="route('profile.edit')" :style="{ color: tok.text, textDecoration: 'none', borderBottom: '1.5px dashed ' + tok.primary }">{{ userName }}</Link> ! 👋
|
||||
</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.muted }">Coach IA · En ligne</div>
|
||||
</div>
|
||||
<div v-if="latest" :style="{
|
||||
background: inRange ? tok.light : tok.amberLight,
|
||||
borderRadius: '14px', padding: '8px 14px', textAlign: 'center',
|
||||
border: '2px solid ' + (inRange ? tok.primary : tok.amber),
|
||||
}">
|
||||
<div :style="{ fontSize: '20px', fontWeight: 800, color: tok.text, lineHeight: 1 }">
|
||||
{{ latest.value }}
|
||||
</div>
|
||||
<div :style="{ fontSize: '9px', color: inRange ? tok.primary : tok.amber, marginTop: '2px' }">
|
||||
mg/dL {{ inRange ? '✓' : '⚠' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ background: tok.light, borderRadius: '16px', padding: '12px 14px' }">
|
||||
<div :style="{ fontSize: '12px', color: tok.text, lineHeight: 1.6 }">
|
||||
🤖 Votre glycémie actuelle est
|
||||
<strong>{{ inRange ? 'dans la cible' : 'à surveiller' }}</strong>.
|
||||
Essayez une marche de 10 min ce soir — je vous rappellerai à 19h30 !
|
||||
</div>
|
||||
<button :style="{
|
||||
marginTop: '10px', padding: '5px 14px', borderRadius: '20px',
|
||||
background: tok.white, border: '1.5px solid ' + tok.primary,
|
||||
fontSize: '11px', color: tok.primary, fontWeight: 600, cursor: 'pointer',
|
||||
}">Ajouter ce défi ✓</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px 20px;display:flex;flex-direction:column;gap:14px;">
|
||||
<!-- Stats row -->
|
||||
<div style="display:flex;gap:14px;">
|
||||
<div :style="{ flex: 1, background: tok.white, borderRadius: '16px', padding: '14px', textAlign: 'center', boxShadow: '0 2px 10px rgba(42,53,51,0.05)' }">
|
||||
<div :style="{ fontSize: '26px', fontWeight: 800, color: tok.text }">🔥 {{ metrics.streak_days }}</div>
|
||||
<div :style="{ fontSize: '10px', color: tok.muted, marginTop: '3px' }">jours dans la cible</div>
|
||||
</div>
|
||||
<div :style="{ flex: 1, background: tok.white, borderRadius: '16px', padding: '14px', textAlign: 'center', boxShadow: '0 2px 10px rgba(42,53,51,0.05)' }">
|
||||
<div :style="{ fontSize: '26px', fontWeight: 800, color: tok.primary }">{{ metrics.time_in_range }}%</div>
|
||||
<div :style="{ fontSize: '10px', color: tok.muted, marginTop: '3px' }">temps dans la cible</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Day chart -->
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '18px 18px 12px', boxShadow: '0 2px 16px rgba(42,53,51,0.06)' }">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 600, color: tok.text }">Aujourd'hui</div>
|
||||
<Link :href="route('stats')" :style="{ fontSize: '11px', color: tok.primary, textDecoration: 'none' }">Détails →</Link>
|
||||
</div>
|
||||
<div v-if="day_chart.length === 0" :style="{ fontSize: '12px', color: tok.muted, textAlign: 'center', padding: '18px 0' }">
|
||||
Aucune mesure aujourd'hui — tapez + pour en ajouter une.
|
||||
</div>
|
||||
<GlucoseChart v-else :data="day_chart" :tok="tok" :width="316" :height="120" :target-min="metrics.target_min" :target-max="metrics.target_max" />
|
||||
</div>
|
||||
|
||||
<!-- Active challenges -->
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 16px rgba(42,53,51,0.06)' }">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 600, color: tok.text }">Défis actifs</div>
|
||||
<Link :href="route('challenges')" :style="{ fontSize: '11px', color: tok.primary, textDecoration: 'none' }">Voir tout →</Link>
|
||||
</div>
|
||||
<div v-if="!challenges.length" :style="{ fontSize: '12px', color: tok.muted }">Aucun défi en cours.</div>
|
||||
<div v-for="(c, i) in challenges" :key="c.id" :style="{ marginBottom: i < challenges.length - 1 ? '14px' : 0, display: 'flex', alignItems: 'center', gap: '10px' }">
|
||||
<div style="font-size:24px;">{{ c.icon }}</div>
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 600, color: tok.text }">{{ c.title }}</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.amber, fontWeight: 600 }">+{{ c.points }} pts</div>
|
||||
</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginBottom: '6px' }">{{ c.description }}</div>
|
||||
<ProgressBar :value="c.progress" :tok="tok" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badges -->
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 16px rgba(42,53,51,0.06)' }">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 600, color: tok.text }">Badges débloqués</div>
|
||||
<Link :href="route('challenges')" :style="{ fontSize: '11px', color: tok.primary, textDecoration: 'none' }">Voir tout →</Link>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-around;">
|
||||
<BadgeChip v-for="(b, i) in badges.slice(0, 4)" :key="i" :icon="b.icon" :label="b.label" :unlocked="b.unlocked" :tok="tok" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="openInput" :style="{
|
||||
width: '100%', padding: '14px', background: tok.primary, color: '#fff',
|
||||
borderRadius: '16px', border: 'none', fontSize: '15px', fontWeight: 600,
|
||||
cursor: 'pointer', boxShadow: '0 6px 18px rgba(123,191,181,0.35)',
|
||||
}">+ Saisir une mesure</button>
|
||||
</div>
|
||||
</DiabetixLayout>
|
||||
</template>
|
||||
113
app/resources/js/Pages/Diabetix/Stats.vue
Normal file
113
app/resources/js/Pages/Diabetix/Stats.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import { Head, router } from '@inertiajs/vue3';
|
||||
import DiabetixLayout from '@/Layouts/DiabetixLayout.vue';
|
||||
import GlucoseChart from '@/Components/Diabetix/GlucoseChart.vue';
|
||||
|
||||
const props = defineProps({
|
||||
period: String,
|
||||
points: Array,
|
||||
stats: Object,
|
||||
history: Array,
|
||||
target: Object,
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ id: 'day', label: 'Auj.' },
|
||||
{ id: 'week', label: 'Sem.' },
|
||||
{ id: 'month', label: 'Mois' },
|
||||
{ id: 'quarter', label: 'Trim.' },
|
||||
];
|
||||
|
||||
function setPeriod(p) {
|
||||
router.get(route('stats'), { period: p }, { preserveState: false, preserveScroll: true });
|
||||
}
|
||||
|
||||
function fmt(v) {
|
||||
return v == null ? '–' : String(v);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Statistiques" />
|
||||
<DiabetixLayout v-slot="{ tok, font }">
|
||||
<div :style="{ background: tok.white, padding: '14px 20px', borderBottom: '1px solid ' + tok.border }">
|
||||
<div :style="{ fontFamily: font.title, fontSize: '20px', fontWeight: 600, color: tok.text, marginBottom: '12px' }">Statistiques</div>
|
||||
<div :style="{ display: 'flex', gap: '3px', background: tok.bgAlt, borderRadius: '12px', padding: '3px' }">
|
||||
<button v-for="t in tabs" :key="t.id" @click="setPeriod(t.id)"
|
||||
:style="{
|
||||
flex: 1, padding: '7px 4px', borderRadius: '10px', border: 'none',
|
||||
background: period === t.id ? tok.primary : 'transparent',
|
||||
color: period === t.id ? '#fff' : tok.muted,
|
||||
fontSize: '12px', fontWeight: period === t.id ? 600 : 400, cursor: 'pointer',
|
||||
}">{{ t.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px 20px;display:flex;flex-direction:column;gap:14px;">
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 14px rgba(42,53,51,0.06)' }">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 600, color: tok.text }">Courbe glycémique</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<span :style="{ display: 'flex', alignItems: 'center', gap: '3px', fontSize: '9px', color: tok.muted }">
|
||||
<span :style="{ width: '7px', height: '7px', borderRadius: '2px', background: tok.primary, display: 'inline-block' }" />Dans la cible
|
||||
</span>
|
||||
<span :style="{ display: 'flex', alignItems: 'center', gap: '3px', fontSize: '9px', color: tok.muted }">
|
||||
<span :style="{ width: '7px', height: '7px', borderRadius: '2px', background: tok.amber, display: 'inline-block' }" />Hors cible
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<GlucoseChart :data="points" :tok="tok" :width="316" :height="120" :target-min="target.min" :target-max="target.max" />
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<div :style="{ background: tok.white, borderRadius: '16px', padding: '14px', boxShadow: '0 2px 10px rgba(42,53,51,0.05)' }">
|
||||
<div style="font-size:16px;margin-bottom:4px;">📊</div>
|
||||
<div :style="{ fontSize: '22px', fontWeight: 800, color: tok.text }">{{ fmt(stats.avg) }}</div>
|
||||
<div :style="{ fontSize: '10px', color: tok.muted, marginTop: '2px' }">Moyenne · mg/dL</div>
|
||||
</div>
|
||||
<div :style="{ background: tok.white, borderRadius: '16px', padding: '14px', boxShadow: '0 2px 10px rgba(42,53,51,0.05)' }">
|
||||
<div style="font-size:16px;margin-bottom:4px;">🎯</div>
|
||||
<div :style="{ fontSize: '22px', fontWeight: 800, color: tok.primary }">{{ stats.in_range_pct }}%</div>
|
||||
<div :style="{ fontSize: '10px', color: tok.muted, marginTop: '2px' }">Dans la cible</div>
|
||||
</div>
|
||||
<div :style="{ background: tok.white, borderRadius: '16px', padding: '14px', boxShadow: '0 2px 10px rgba(42,53,51,0.05)' }">
|
||||
<div style="font-size:16px;margin-bottom:4px;">⬇️</div>
|
||||
<div :style="{ fontSize: '22px', fontWeight: 800, color: tok.amber }">{{ fmt(stats.min) }}</div>
|
||||
<div :style="{ fontSize: '10px', color: tok.muted, marginTop: '2px' }">Minimum · mg/dL</div>
|
||||
</div>
|
||||
<div :style="{ background: tok.white, borderRadius: '16px', padding: '14px', boxShadow: '0 2px 10px rgba(42,53,51,0.05)' }">
|
||||
<div style="font-size:16px;margin-bottom:4px;">⬆️</div>
|
||||
<div :style="{ fontSize: '22px', fontWeight: 800, color: '#d4826a' }">{{ fmt(stats.max) }}</div>
|
||||
<div :style="{ fontSize: '10px', color: tok.muted, marginTop: '2px' }">Maximum · mg/dL</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 14px rgba(42,53,51,0.06)' }">
|
||||
<div :style="{ fontSize: '13px', fontWeight: 600, color: tok.text, marginBottom: '14px' }">Historique</div>
|
||||
<div v-for="(e, i) in history" :key="e.id"
|
||||
:style="{
|
||||
display: 'flex', alignItems: 'center', gap: '12px',
|
||||
paddingBottom: i < history.length - 1 ? '12px' : 0,
|
||||
borderBottom: i < history.length - 1 ? '1px solid ' + tok.border : 'none',
|
||||
marginBottom: i < history.length - 1 ? '12px' : 0,
|
||||
}">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;width:12px;">
|
||||
<div :style="{ width: '10px', height: '10px', borderRadius: '50%', background: e.in_range ? tok.primary : tok.amber }" />
|
||||
<div v-if="i < history.length - 1" :style="{ width: '1px', height: '24px', background: tok.border, marginTop: '2px' }" />
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<span :style="{ fontSize: '14px', fontWeight: 700, color: tok.text }">{{ fmt(e.value) }} </span>
|
||||
<span :style="{ fontSize: '11px', color: tok.muted }">mg/dL </span>
|
||||
<span v-if="!e.in_range" :style="{ fontSize: '11px', color: tok.amber, fontWeight: 600 }">⚠️</span>
|
||||
</div>
|
||||
<span :style="{ fontSize: '11px', color: tok.muted }">{{ e.time }}</span>
|
||||
</div>
|
||||
<div :style="{ fontSize: '11px', color: tok.muted }">{{ e.context }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DiabetixLayout>
|
||||
</template>
|
||||
279
app/resources/js/Pages/Profile/Edit.vue
Normal file
279
app/resources/js/Pages/Profile/Edit.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
|
||||
import DiabetixLayout from '@/Layouts/DiabetixLayout.vue';
|
||||
|
||||
defineProps({
|
||||
mustVerifyEmail: Boolean,
|
||||
status: String,
|
||||
});
|
||||
|
||||
const user = usePage().props.auth.user;
|
||||
|
||||
// Formulaire infos compte
|
||||
const accountForm = useForm({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
// Formulaire préférences Diabetix
|
||||
const diabetixForm = 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',
|
||||
});
|
||||
|
||||
// Formulaire mot de passe
|
||||
const passwordForm = useForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const passwordRef = ref(null);
|
||||
const currentPasswordRef = ref(null);
|
||||
|
||||
function updatePassword() {
|
||||
passwordForm.put(route('password.update'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => passwordForm.reset(),
|
||||
onError: () => {
|
||||
if (passwordForm.errors.password) {
|
||||
passwordForm.reset('password', 'password_confirmation');
|
||||
passwordRef.value?.focus();
|
||||
}
|
||||
if (passwordForm.errors.current_password) {
|
||||
passwordForm.reset('current_password');
|
||||
currentPasswordRef.value?.focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Suppression compte
|
||||
const deleteOpen = ref(false);
|
||||
const deletePasswordRef = ref(null);
|
||||
const deleteForm = useForm({ password: '' });
|
||||
|
||||
function openDelete() {
|
||||
deleteOpen.value = true;
|
||||
nextTick(() => deletePasswordRef.value?.focus());
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
deleteForm.delete(route('profile.destroy'), {
|
||||
preserveScroll: true,
|
||||
onError: () => deletePasswordRef.value?.focus(),
|
||||
onFinish: () => deleteForm.reset(),
|
||||
});
|
||||
}
|
||||
|
||||
const palettes = [
|
||||
{ id: 'mint', label: 'Menthe', swatch: '#7bbfb5' },
|
||||
{ id: 'lilac', label: 'Lilas', swatch: '#9b8ec4' },
|
||||
{ id: 'peach', label: 'Pêche', swatch: '#d4826a' },
|
||||
];
|
||||
|
||||
const diabetesTypes = [
|
||||
{ id: 'type1', label: 'Type 1' },
|
||||
{ id: 'type2', label: 'Type 2' },
|
||||
{ id: 'gestational', label: 'Gestationnel' },
|
||||
{ id: 'other', label: 'Autre' },
|
||||
];
|
||||
|
||||
function inputStyle(tok) {
|
||||
return {
|
||||
width: '100%', padding: '11px 14px', borderRadius: '12px',
|
||||
border: '1.5px solid ' + tok.border, background: tok.bg,
|
||||
fontSize: '14px', color: tok.text, outline: 'none', boxSizing: 'border-box',
|
||||
};
|
||||
}
|
||||
|
||||
function chipStyle(active, tok) {
|
||||
return {
|
||||
padding: '7px 14px', borderRadius: '20px',
|
||||
border: '1.5px solid ' + (active ? tok.primary : tok.border),
|
||||
background: active ? tok.light : tok.white,
|
||||
color: active ? tok.dark : tok.muted,
|
||||
fontSize: '12px', fontWeight: active ? 600 : 400, cursor: 'pointer',
|
||||
};
|
||||
}
|
||||
|
||||
function btnPrimary(tok, disabled) {
|
||||
return {
|
||||
padding: '13px 20px', background: tok.primary, color: '#fff',
|
||||
borderRadius: '14px', border: 'none', fontSize: '13px', fontWeight: 600,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.6 : 1,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Paramètres" />
|
||||
<DiabetixLayout v-slot="{ tok, font }">
|
||||
|
||||
<!-- Header -->
|
||||
<div :style="{ background: tok.white, padding: '14px 20px 18px', borderBottom: '1px solid ' + tok.border }">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<Link :href="route('dashboard')" :style="{ color: tok.muted, fontSize: '20px', textDecoration: 'none', lineHeight: 1 }">←</Link>
|
||||
<div :style="{ fontFamily: font.title, fontSize: '20px', fontWeight: 600, color: tok.text }">Paramètres</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding:14px 20px;display:flex;flex-direction:column;gap:14px;">
|
||||
|
||||
<!-- Préférences Diabetix -->
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 16px rgba(42,53,51,0.06)' }">
|
||||
<div :style="{ fontSize: '11px', fontWeight: 700, color: tok.muted, letterSpacing: '0.8px', textTransform: 'uppercase', marginBottom: '16px' }">Mes préférences</div>
|
||||
|
||||
<form @submit.prevent="diabetixForm.patch(route('profile.diabetix'), { preserveScroll: true })">
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px' }">Prénom</label>
|
||||
<input v-model="diabetixForm.first_name" type="text" :style="inputStyle(tok)" placeholder="Marie" />
|
||||
<span v-if="diabetixForm.errors.first_name" :style="{ fontSize: '11px', color: '#c43' }">{{ diabetixForm.errors.first_name }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '8px' }">Type de diabète</label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;">
|
||||
<button v-for="t in diabetesTypes" :key="t.id" type="button" @click="diabetixForm.diabetes_type = t.id" :style="chipStyle(diabetixForm.diabetes_type === t.id, tok)">{{ t.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '8px' }">Plage cible glycémique (mg/dL)</label>
|
||||
<div style="display:flex;gap:10px;align-items:center;">
|
||||
<input v-model.number="diabetixForm.target_min" type="number" min="50" max="140" :style="{ ...inputStyle(tok), textAlign: 'center', width: '80px' }" />
|
||||
<span :style="{ color: tok.muted, fontSize: '13px' }">–</span>
|
||||
<input v-model.number="diabetixForm.target_max" type="number" min="120" max="300" :style="{ ...inputStyle(tok), textAlign: 'center', width: '80px' }" />
|
||||
<span :style="{ color: tok.muted, fontSize: '11px' }">mg/dL</span>
|
||||
</div>
|
||||
<span v-if="diabetixForm.errors.target_min || diabetixForm.errors.target_max" :style="{ fontSize: '11px', color: '#c43' }">{{ diabetixForm.errors.target_min || diabetixForm.errors.target_max }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '8px' }">Thème de couleur</label>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<button v-for="p in palettes" :key="p.id" type="button" @click="diabetixForm.palette = p.id"
|
||||
:style="{
|
||||
display: 'flex', alignItems: 'center', gap: '7px',
|
||||
padding: '8px 14px', borderRadius: '20px',
|
||||
border: '1.5px solid ' + (diabetixForm.palette === p.id ? tok.text : tok.border),
|
||||
background: diabetixForm.palette === p.id ? tok.light : tok.white,
|
||||
fontSize: '12px', fontWeight: diabetixForm.palette === p.id ? 600 : 400,
|
||||
color: tok.text, cursor: 'pointer',
|
||||
}">
|
||||
<span :style="{ width: '12px', height: '12px', borderRadius: '50%', background: p.swatch, display: 'inline-block' }" />
|
||||
{{ p.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<button type="submit" :disabled="diabetixForm.processing" :style="btnPrimary(tok, diabetixForm.processing)">
|
||||
{{ diabetixForm.processing ? '…' : 'Enregistrer' }}
|
||||
</button>
|
||||
<span v-if="diabetixForm.recentlySuccessful" :style="{ fontSize: '12px', color: tok.primary }">✓ Enregistré</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Infos compte -->
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 16px rgba(42,53,51,0.06)' }">
|
||||
<div :style="{ fontSize: '11px', fontWeight: 700, color: tok.muted, letterSpacing: '0.8px', textTransform: 'uppercase', marginBottom: '16px' }">Mon compte</div>
|
||||
|
||||
<form @submit.prevent="accountForm.patch(route('profile.update'), { preserveScroll: true })">
|
||||
<div style="margin-bottom:14px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px' }">Nom complet</label>
|
||||
<input v-model="accountForm.name" type="text" required :style="inputStyle(tok)" />
|
||||
<span v-if="accountForm.errors.name" :style="{ fontSize: '11px', color: '#c43' }">{{ accountForm.errors.name }}</span>
|
||||
</div>
|
||||
<div style="margin-bottom:20px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px' }">Adresse e-mail</label>
|
||||
<input v-model="accountForm.email" type="email" required :style="inputStyle(tok)" />
|
||||
<span v-if="accountForm.errors.email" :style="{ fontSize: '11px', color: '#c43' }">{{ accountForm.errors.email }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<button type="submit" :disabled="accountForm.processing" :style="btnPrimary(tok, accountForm.processing)">
|
||||
{{ accountForm.processing ? '…' : 'Mettre à jour' }}
|
||||
</button>
|
||||
<span v-if="accountForm.recentlySuccessful" :style="{ fontSize: '12px', color: tok.primary }">✓ Enregistré</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Mot de passe -->
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 16px rgba(42,53,51,0.06)' }">
|
||||
<div :style="{ fontSize: '11px', fontWeight: 700, color: tok.muted, letterSpacing: '0.8px', textTransform: 'uppercase', marginBottom: '16px' }">Mot de passe</div>
|
||||
|
||||
<form @submit.prevent="updatePassword">
|
||||
<div style="margin-bottom:12px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px' }">Mot de passe actuel</label>
|
||||
<input ref="currentPasswordRef" v-model="passwordForm.current_password" type="password" autocomplete="current-password" :style="inputStyle(tok)" />
|
||||
<span v-if="passwordForm.errors.current_password" :style="{ fontSize: '11px', color: '#c43' }">{{ passwordForm.errors.current_password }}</span>
|
||||
</div>
|
||||
<div style="margin-bottom:12px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px' }">Nouveau mot de passe</label>
|
||||
<input ref="passwordRef" v-model="passwordForm.password" type="password" autocomplete="new-password" :style="inputStyle(tok)" />
|
||||
<span v-if="passwordForm.errors.password" :style="{ fontSize: '11px', color: '#c43' }">{{ passwordForm.errors.password }}</span>
|
||||
</div>
|
||||
<div style="margin-bottom:20px;">
|
||||
<label :style="{ fontSize: '11px', color: tok.muted, fontWeight: 600, display: 'block', marginBottom: '6px' }">Confirmer le nouveau mot de passe</label>
|
||||
<input v-model="passwordForm.password_confirmation" type="password" autocomplete="new-password" :style="inputStyle(tok)" />
|
||||
<span v-if="passwordForm.errors.password_confirmation" :style="{ fontSize: '11px', color: '#c43' }">{{ passwordForm.errors.password_confirmation }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<button type="submit" :disabled="passwordForm.processing" :style="btnPrimary(tok, passwordForm.processing)">
|
||||
{{ passwordForm.processing ? '…' : 'Changer le mot de passe' }}
|
||||
</button>
|
||||
<span v-if="passwordForm.recentlySuccessful" :style="{ fontSize: '12px', color: tok.primary }">✓ Modifié</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Session -->
|
||||
<div :style="{ background: tok.white, borderRadius: '20px', padding: '20px', boxShadow: '0 2px 16px rgba(42,53,51,0.06)' }">
|
||||
<div :style="{ fontSize: '11px', fontWeight: 700, color: tok.muted, letterSpacing: '0.8px', textTransform: 'uppercase', marginBottom: '16px' }">Session</div>
|
||||
|
||||
<Link :href="route('logout')" method="post" as="button"
|
||||
:style="{ ...btnPrimary(tok, false), background: tok.bgAlt, color: tok.text, boxShadow: 'none', width: '100%', marginBottom: '10px', display: 'block', textAlign: 'center', textDecoration: 'none' }">
|
||||
Se déconnecter
|
||||
</Link>
|
||||
|
||||
<button @click="openDelete"
|
||||
:style="{ width: '100%', padding: '13px', background: 'transparent', border: '1.5px solid #fb9999', borderRadius: '14px', color: '#c43', fontSize: '13px', fontWeight: 600, cursor: 'pointer' }">
|
||||
Supprimer mon compte
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal suppression -->
|
||||
<div v-if="deleteOpen" @click.self="deleteOpen = false"
|
||||
:style="{ position: 'fixed', inset: 0, zIndex: 50, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }">
|
||||
<div :style="{ width: '100%', maxWidth: '440px', background: tok.white, borderRadius: '24px 24px 0 0', padding: '6px 20px 36px', boxShadow: '0 -8px 40px rgba(0,0,0,0.18)' }">
|
||||
<div :style="{ width: '40px', height: '4px', borderRadius: '2px', background: tok.border, margin: '12px auto 20px' }" />
|
||||
<div :style="{ fontFamily: font.title, fontSize: '18px', fontWeight: 600, color: tok.text, marginBottom: '8px' }">Supprimer mon compte</div>
|
||||
<p :style="{ fontSize: '13px', color: tok.muted, marginBottom: '20px', lineHeight: 1.5 }">
|
||||
Cette action est irréversible. Toutes vos données (mesures, défis, badges) seront définitivement supprimées. Confirmez votre mot de passe pour continuer.
|
||||
</p>
|
||||
<input ref="deletePasswordRef" v-model="deleteForm.password" type="password" placeholder="Mot de passe"
|
||||
:style="{ ...inputStyle(tok), marginBottom: '8px' }" @keyup.enter="confirmDelete" />
|
||||
<span v-if="deleteForm.errors.password" :style="{ fontSize: '11px', color: '#c43', display: 'block', marginBottom: '10px' }">{{ deleteForm.errors.password }}</span>
|
||||
<div style="display:flex;gap:10px;margin-top:12px;">
|
||||
<button @click="deleteOpen = false"
|
||||
:style="{ flex: 1, padding: '13px', background: tok.bgAlt, border: 'none', borderRadius: '14px', fontSize: '13px', color: tok.text, fontWeight: 600, cursor: 'pointer' }">
|
||||
Annuler
|
||||
</button>
|
||||
<button @click="confirmDelete" :disabled="deleteForm.processing"
|
||||
:style="{ flex: 1, padding: '13px', background: '#c43', border: 'none', borderRadius: '14px', fontSize: '13px', color: '#fff', fontWeight: 600, cursor: 'pointer', opacity: deleteForm.processing ? 0.6 : 1 }">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</DiabetixLayout>
|
||||
</template>
|
||||
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>
|
||||
39
app/resources/js/Pages/Welcome.vue
Normal file
39
app/resources/js/Pages/Welcome.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup>
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { tokens, FONT } from '@/Components/Diabetix/palette.js';
|
||||
|
||||
defineProps({
|
||||
canLogin: Boolean,
|
||||
canRegister: Boolean,
|
||||
});
|
||||
|
||||
const tok = tokens('mint');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Diabetix — Coach glycémique IA" />
|
||||
<div :style="{ minHeight: '100vh', background: '#eeece8', fontFamily: FONT.body, display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '24px' }">
|
||||
<div :style="{ maxWidth: '440px', width: '100%', background: tok.bg, borderRadius: '32px', padding: '40px 28px', boxShadow: '0 12px 60px rgba(42,53,51,0.10)' }">
|
||||
<div :style="{ width: '72px', height: '72px', borderRadius: '50%', background: tok.light, border: '3px solid ' + tok.primary, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '36px', margin: '0 auto 20px' }">🤖</div>
|
||||
<h1 :style="{ fontFamily: FONT.title, fontSize: '32px', fontWeight: 700, color: tok.text, textAlign: 'center', marginBottom: '8px' }">Diabetix</h1>
|
||||
<p :style="{ textAlign: 'center', color: tok.muted, fontSize: '14px', marginBottom: '28px', lineHeight: 1.6 }">
|
||||
Votre coach glycémique IA, pensé pour vous accompagner au quotidien dans la gestion de votre diabète.
|
||||
</p>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<Link v-if="canLogin" :href="route('login')"
|
||||
:style="{ display: 'block', textAlign: 'center', padding: '14px', background: tok.primary, color: '#fff', borderRadius: '16px', textDecoration: 'none', fontWeight: 600, fontSize: '15px' }">
|
||||
Se connecter
|
||||
</Link>
|
||||
<Link v-if="canRegister" :href="route('register')"
|
||||
:style="{ display: 'block', textAlign: 'center', padding: '14px', background: tok.white, color: tok.primary, border: '1.5px solid ' + tok.primary, borderRadius: '16px', textDecoration: 'none', fontWeight: 600, fontSize: '15px' }">
|
||||
Créer un compte
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div :style="{ marginTop: '28px', fontSize: '11px', color: tok.muted, textAlign: 'center' }">
|
||||
Suivi des glycémies · Défis · Coach IA · Statistiques
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user