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:
21
app/resources/js/Components/Diabetix/BadgeChip.vue
Normal file
21
app/resources/js/Components/Diabetix/BadgeChip.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: String,
|
||||
label: String,
|
||||
unlocked: { type: Boolean, default: false },
|
||||
tok: { type: Object, required: true },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:5px;">
|
||||
<div :style="{
|
||||
width: '46px', height: '46px', borderRadius: '50%',
|
||||
background: unlocked ? tok.light : tok.bgAlt,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '20px',
|
||||
filter: unlocked ? 'none' : 'grayscale(1) opacity(0.35)',
|
||||
border: '2px solid ' + (unlocked ? tok.primary : tok.border),
|
||||
}">{{ icon }}</div>
|
||||
<span :style="{ fontSize: '9px', color: tok.muted, textAlign: 'center', lineHeight: 1.2, maxWidth: '52px' }">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
15
app/resources/js/Components/Diabetix/CoachAvatar.vue
Normal file
15
app/resources/js/Components/Diabetix/CoachAvatar.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
size: { type: Number, default: 36 },
|
||||
tok: { type: Object, required: true },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{
|
||||
width: size + 'px', height: size + 'px', borderRadius: '50%',
|
||||
background: tok.light, flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: (size * 0.44) + 'px', border: '2px solid ' + tok.primary,
|
||||
}">🤖</div>
|
||||
</template>
|
||||
53
app/resources/js/Components/Diabetix/GlucoseChart.vue
Normal file
53
app/resources/js/Components/Diabetix/GlucoseChart.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, default: () => [] },
|
||||
tok: { type: Object, required: true },
|
||||
width: { type: Number, default: 300 },
|
||||
height: { type: Number, default: 110 },
|
||||
targetMin: { type: Number, default: 70 },
|
||||
targetMax: { type: Number, default: 180 },
|
||||
});
|
||||
|
||||
const PAD = { t: 10, r: 18, b: 24, l: 28 };
|
||||
|
||||
const computed_ = computed(() => {
|
||||
const cW = props.width - PAD.l - PAD.r;
|
||||
const cH = props.height - PAD.t - PAD.b;
|
||||
const data = props.data.length ? props.data : [{ l: '–', v: props.targetMin }];
|
||||
const values = data.map(d => d.v);
|
||||
// Sensible mg/dL window with auto-extension when values exceed bounds.
|
||||
const MIN_V = Math.min(50, Math.floor((Math.min(...values) - 10) / 10) * 10);
|
||||
const MAX_V = Math.max(220, Math.ceil((Math.max(...values) + 10) / 10) * 10);
|
||||
const yS = v => cH - ((v - MIN_V) / (MAX_V - MIN_V)) * cH;
|
||||
const xS = i => (i / Math.max(data.length - 1, 1)) * cW;
|
||||
const pts = data.map((d, i) => ({ x: xS(i), y: yS(d.v), v: d.v, l: d.l }));
|
||||
|
||||
let line = `M ${pts[0].x} ${pts[0].y}`;
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
const cx = (pts[i - 1].x + pts[i].x) / 2;
|
||||
line += ` C ${cx} ${pts[i - 1].y} ${cx} ${pts[i].y} ${pts[i].x} ${pts[i].y}`;
|
||||
}
|
||||
const area = `${line} L ${pts[pts.length - 1].x} ${cH} L ${pts[0].x} ${cH} Z`;
|
||||
return { cW, cH, pts, line, area, ty1: yS(props.targetMax), ty2: yS(props.targetMin), yS };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg :width="width" :height="height" style="display:block;overflow:visible">
|
||||
<g :transform="`translate(${PAD.l},${PAD.t})`">
|
||||
<line v-for="v in [70,120,180,240]" :key="v" :x1="0" :y1="computed_.yS(v)" :x2="computed_.cW" :y2="computed_.yS(v)" :stroke="tok.border" stroke-width="0.8" stroke-dasharray="3,3" />
|
||||
<rect :x="0" :y="computed_.ty1" :width="computed_.cW" :height="computed_.ty2 - computed_.ty1" :fill="tok.primary" opacity="0.13" rx="3" />
|
||||
<text :x="computed_.cW + 3" :y="computed_.ty1 + 4" font-size="7" :fill="tok.primary">{{ targetMax }}</text>
|
||||
<text :x="computed_.cW + 3" :y="computed_.ty2 + 4" font-size="7" :fill="tok.primary">{{ targetMin }}</text>
|
||||
<path :d="computed_.area" :fill="tok.primary" opacity="0.09" />
|
||||
<path :d="computed_.line" fill="none" :stroke="tok.primary" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle v-for="(p, i) in computed_.pts" :key="'c'+i" :cx="p.x" :cy="p.y" r="3.5"
|
||||
:fill="p.v >= targetMin && p.v <= targetMax ? tok.primary : tok.amber"
|
||||
:stroke="tok.white" stroke-width="1.5" />
|
||||
<text v-for="(p, i) in computed_.pts" :key="'l'+i" :x="p.x" :y="computed_.cH + 14" text-anchor="middle" font-size="8" :fill="tok.muted">{{ p.l }}</text>
|
||||
<text v-for="v in [70,180]" :key="'y'+v" :x="-4" :y="computed_.yS(v) + 3" text-anchor="end" font-size="8" :fill="tok.muted">{{ v }}</text>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
90
app/resources/js/Components/Diabetix/InputModal.vue
Normal file
90
app/resources/js/Components/Diabetix/InputModal.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup>
|
||||
import { reactive } from 'vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
tok: { type: Object, required: true },
|
||||
font: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const form = useForm({
|
||||
value: '',
|
||||
context: 'before_meal',
|
||||
carbs_g: '',
|
||||
insulin_units: '',
|
||||
});
|
||||
|
||||
const contexts = [
|
||||
{ id: 'fasting', label: 'À jeun' },
|
||||
{ id: 'before_meal', label: 'Avant repas' },
|
||||
{ id: 'after_meal', label: 'Après repas' },
|
||||
{ id: 'bedtime', label: 'Coucher' },
|
||||
];
|
||||
|
||||
function submit() {
|
||||
form.post(route('measurements.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
emit('close');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function close(e) {
|
||||
if (e.target === e.currentTarget) emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="close" :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: '20px', fontWeight: 600, color: tok.text, marginBottom: '20px' }">Saisir une mesure</div>
|
||||
|
||||
<div :style="{ background: tok.bg, borderRadius: '20px', padding: '16px 20px', marginBottom: '16px', textAlign: 'center' }">
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '0.8px', fontWeight: 600 }">Glycémie (mg/dL)</div>
|
||||
<input v-model="form.value" type="number" step="1" min="20" max="600" placeholder="105"
|
||||
:style="{ width: '100%', textAlign: 'center', fontSize: '52px', fontWeight: 800, color: tok.text, border: 'none', background: 'transparent', outline: 'none' }" />
|
||||
<div v-if="form.errors.value" :style="{ color: '#c43', fontSize: '11px' }">{{ form.errors.value }}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:16px;">
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginBottom: '8px', textTransform: 'uppercase', letterSpacing: '0.8px', fontWeight: 600 }">Contexte</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;">
|
||||
<button v-for="c in contexts" :key="c.id" @click="form.context = c.id" type="button"
|
||||
:style="{
|
||||
padding: '7px 14px', borderRadius: '20px',
|
||||
border: '1.5px solid ' + (form.context === c.id ? tok.primary : tok.border),
|
||||
background: form.context === c.id ? tok.light : tok.white,
|
||||
color: form.context === c.id ? tok.dark : tok.muted,
|
||||
fontSize: '12px', fontWeight: form.context === c.id ? 600 : 400, cursor: 'pointer',
|
||||
}">{{ c.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px;margin-bottom:20px;">
|
||||
<div style="flex:1;">
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginBottom: '6px' }">Glucides (g) · optionnel</div>
|
||||
<input v-model="form.carbs_g" type="number" placeholder="45"
|
||||
:style="{ width: '100%', padding: '10px 12px', borderRadius: '12px', border: '1.5px solid ' + tok.border, background: tok.bg, fontSize: '14px', color: tok.text, outline: 'none', boxSizing: 'border-box' }" />
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div :style="{ fontSize: '11px', color: tok.muted, marginBottom: '6px' }">Insuline (u) · optionnel</div>
|
||||
<input v-model="form.insulin_units" type="number" step="0.5" placeholder="4"
|
||||
:style="{ width: '100%', padding: '10px 12px', borderRadius: '12px', border: '1.5px solid ' + tok.border, background: tok.bg, fontSize: '14px', color: tok.text, outline: 'none', boxSizing: 'border-box' }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="submit" :disabled="form.processing"
|
||||
:style="{ width: '100%', padding: '16px', background: tok.primary, color: '#fff', borderRadius: '16px', border: 'none', fontSize: '16px', fontWeight: 600, cursor: 'pointer', opacity: form.processing ? 0.6 : 1 }">
|
||||
Enregistrer la mesure ✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
20
app/resources/js/Components/Diabetix/ProgressBar.vue
Normal file
20
app/resources/js/Components/Diabetix/ProgressBar.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: Number, default: 0 },
|
||||
max: { type: Number, default: 100 },
|
||||
color: { type: String, default: null },
|
||||
height: { type: Number, default: 6 },
|
||||
bg: { type: String, default: null },
|
||||
tok: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const pct = computed(() => Math.min(100, Math.max(0, (props.value / props.max) * 100)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{ height: height + 'px', borderRadius: '999px', overflow: 'hidden', background: bg || tok.bgAlt }">
|
||||
<div :style="{ height: '100%', width: pct + '%', background: color || tok.primary, borderRadius: '999px', transition: 'width .3s ease' }" />
|
||||
</div>
|
||||
</template>
|
||||
33
app/resources/js/Components/Diabetix/palette.js
Normal file
33
app/resources/js/Components/Diabetix/palette.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Diabetix design tokens — recreated from the design bundle.
|
||||
export const PALETTES = {
|
||||
mint: {
|
||||
primary: '#7bbfb5', light: '#d4ede8', dark: '#5ca89d',
|
||||
amber: '#f59e0b', amberLight: '#fef3c7',
|
||||
alert: '#fde8e8', alertDark: '#fb9999',
|
||||
bg: '#faf8f4', bgAlt: '#f0ede6',
|
||||
text: '#2a3533', muted: '#7a9c97',
|
||||
border: '#ddeae7', white: '#ffffff',
|
||||
},
|
||||
lilac: {
|
||||
primary: '#9b8ec4', light: '#ede8f8', dark: '#7b6ea4',
|
||||
amber: '#f59e0b', amberLight: '#fef3c7',
|
||||
alert: '#f8e8fd', alertDark: '#e9a8e9',
|
||||
bg: '#faf8fd', bgAlt: '#f0edf8',
|
||||
text: '#2a2733', muted: '#7a7a9c',
|
||||
border: '#dddaea', white: '#ffffff',
|
||||
},
|
||||
peach: {
|
||||
primary: '#d4826a', light: '#fde8df', dark: '#b56650',
|
||||
amber: '#6bbbaf', amberLight: '#d4ede8',
|
||||
alert: '#fde8e8', alertDark: '#fb9999',
|
||||
bg: '#fdf8f5', bgAlt: '#f5ede7',
|
||||
text: '#332a27', muted: '#9c7a70',
|
||||
border: '#eaddd7', white: '#ffffff',
|
||||
},
|
||||
};
|
||||
|
||||
export const FONT = { title: "'Lora', serif", body: "'Plus Jakarta Sans', sans-serif" };
|
||||
|
||||
export function tokens(palette = 'mint') {
|
||||
return PALETTES[palette] ?? PALETTES.mint;
|
||||
}
|
||||
Reference in New Issue
Block a user