diff --git a/front/src/components/common/ActionSheetModal.js b/front/src/components/common/ActionSheetModal.js index ef3f7d2..9e186d5 100644 --- a/front/src/components/common/ActionSheetModal.js +++ b/front/src/components/common/ActionSheetModal.js @@ -1,5 +1,6 @@ import React from 'react'; import { View, Text, StyleSheet, Modal, TouchableOpacity } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Colors } from '../../constants/theme'; // ─── ActionSheetModal ───────────────────────────────────────────────────────── @@ -16,6 +17,8 @@ import { Colors } from '../../constants/theme'; // onClose () => void — appelé après tout choix (y compris Annuler) export default function ActionSheetModal({ visible, title, options = [], cancelLabel = 'Annuler', onClose }) { + const insets = useSafeAreaInsets(); + const handlePress = (opt) => { onClose?.(); opt.onPress?.(); @@ -23,7 +26,7 @@ export default function ActionSheetModal({ visible, title, options = [], cancelL return ( - + {!!title && {title}} @@ -52,7 +55,12 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: 'rgba(0,0,0,0.82)', justifyContent: 'flex-end', - padding: 12, + paddingHorizontal: 12, + // zIndex/elevation explicites : garantit que la feuille passe au-dessus de + // tout autre contenu positionné (barre d'onglets, headers) — notamment sur + // web où le portail du Modal n'a pas de z-index par défaut. + zIndex: 999, + elevation: 999, }, card: { width: '100%', @@ -63,7 +71,6 @@ const styles = StyleSheet.create({ paddingTop: 18, paddingHorizontal: 8, paddingBottom: 8, - marginBottom: 8, }, title: { color: Colors.textMuted, diff --git a/front/src/components/workouts/SaveWorkoutPromptModal.js b/front/src/components/workouts/SaveWorkoutPromptModal.js new file mode 100644 index 0000000..ac8ae0b --- /dev/null +++ b/front/src/components/workouts/SaveWorkoutPromptModal.js @@ -0,0 +1,161 @@ +import React from 'react'; +import { View, Text, StyleSheet, Modal, TouchableOpacity } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { Colors } from '../../constants/theme'; + +// ─── SaveWorkoutPromptModal ─────────────────────────────────────────────────── +// Interception du lancement d'une séance sur-mesure (WorkoutBuilderScreen) : +// avant d'ouvrir le chrono, propose de sauvegarder la séance générée comme +// template réutilisable. Deux CTA de poids égal (contrairement à +// ConfirmModal/InfoModal qui n'ont qu'une seule action principale) + une +// icône de fermeture pour annuler sans choisir. +// +// Props : +// visible bool +// onSaveAndLaunch () => void — sauvegarde le template PUIS lance la séance +// onLaunchWithoutSave () => void — lance directement, aucun template créé +// onDismiss () => void — ferme la popup sans lancer la séance +// saving bool — désactive les actions pendant la sauvegarde + +export default function SaveWorkoutPromptModal({ + visible, onSaveAndLaunch, onLaunchWithoutSave, onDismiss, saving = false, +}) { + return ( + + + + + + + + + + + + Sauvegarder cette séance ? + + Souhaites-tu sauvegarder cette séance sur-mesure pour la retrouver plus tard ? + + + + + + {saving ? 'Sauvegarde...' : 'Sauvegarder et lancer la séance'} + + + + + + Lancer sans sauvegarder + + + + + ); +} + +const styles = StyleSheet.create({ + backdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.82)', + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 24, + }, + card: { + width: '100%', + backgroundColor: Colors.bgDeep2, + borderRadius: 22, + borderWidth: 1, + borderColor: `${Colors.primary}38`, + padding: 28, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 16 }, + shadowOpacity: 0.65, + shadowRadius: 32, + elevation: 20, + }, + closeBtn: { + position: 'absolute', + top: 16, + right: 16, + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: Colors.glassBg, + justifyContent: 'center', + alignItems: 'center', + zIndex: 1, + }, + iconWrap: { + width: 60, + height: 60, + borderRadius: 18, + borderWidth: 1, + borderColor: `${Colors.primary}45`, + backgroundColor: `${Colors.primary}1A`, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 18, + }, + title: { + color: Colors.textPrimary, + fontSize: 18, + fontWeight: '800', + letterSpacing: -0.3, + marginBottom: 10, + textAlign: 'center', + }, + body: { + color: Colors.textSecondary, + fontSize: 14, + lineHeight: 21, + textAlign: 'center', + marginBottom: 24, + }, + primaryBtn: { + flexDirection: 'row', + width: '100%', + height: 50, + borderRadius: 13, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 10, + backgroundColor: Colors.primary, + shadowColor: Colors.primary, + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.40, + shadowRadius: 12, + elevation: 6, + }, + primaryTxt: { color: '#fff', fontSize: 15, fontWeight: '700', letterSpacing: 0.2 }, + secondaryBtn: { + flexDirection: 'row', + width: '100%', + height: 50, + borderRadius: 13, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: Colors.borderDim, + }, + secondaryTxt: { color: Colors.textPrimary, fontSize: 15, fontWeight: '700', letterSpacing: 0.2 }, + btnDisabled: { opacity: 0.5 }, +}); diff --git a/front/src/data/exerciseCatalog.js b/front/src/data/exerciseCatalog.js index 1c05a29..317d3cf 100644 --- a/front/src/data/exerciseCatalog.js +++ b/front/src/data/exerciseCatalog.js @@ -133,7 +133,6 @@ export const BUILTIN_CATALOG = [ exo('Élévations latérales unilatérales poulie', 'epaules', 'Deltoïde latéral', [], ['Câble'], 'intermediaire'), exo('Oiseau haltères', 'epaules', 'Deltoïde postérieur', ['Trapèzes', 'Rhomboïdes'], ['Haltères'], 'intermediaire', { videoQuery: 'oiseau haltères deltoïde postérieur' }), exo('Oiseau buste penché', 'epaules', 'Deltoïde postérieur', ['Trapèzes'], ['Haltères'], 'intermediaire'), - exo('Oiseau machine', 'epaules', 'Deltoïde postérieur', ['Trapèzes'], ['Machine'], 'debutant'), exo('Reverse pec deck', 'epaules', 'Deltoïde postérieur', ['Trapèzes'], ['Machine'], 'debutant'), exo('Face pull', 'epaules', 'Deltoïde postérieur', ['Trapèzes', 'Rhomboïdes'], ['Câble'], 'debutant'), exo('Rowing menton', 'epaules', 'Deltoïde latéral', ['Trapèzes'], ['Barre'], 'intermediaire'), @@ -162,6 +161,7 @@ export const BUILTIN_CATALOG = [ exo('Triceps poulie corde', 'bras', 'Triceps', [], ['Câble'], 'debutant'), exo('Triceps poulie inversé', 'bras', 'Triceps', [], ['Câble'], 'debutant'), exo('Dips', 'bras', 'Triceps', ['Pectoraux bas', 'Deltoïde antérieur'], ['Poids du corps'], 'intermediaire'), + exo('Dips assistés machine', 'bras', 'Triceps', ['Pectoraux bas', 'Deltoïde antérieur'], ['Machine'], 'debutant'), exo('Dips bench', 'bras', 'Triceps', ['Deltoïde antérieur'], ['Poids du corps'], 'debutant'), exo('Pompes diamant', 'bras', 'Triceps', ['Pectoraux milieu'], ['Poids du corps'], 'intermediaire'), exo('Kickback haltère', 'bras', 'Triceps', [], ['Haltères'], 'debutant'), @@ -186,7 +186,7 @@ export const BUILTIN_CATALOG = [ exo('Step-up', 'jambes', 'Quadriceps', ['Fessiers'], ['Haltères'], 'debutant'), exo('Wall sit', 'jambes', 'Quadriceps', [], ['Poids du corps'], 'debutant'), exo('Leg extension', 'jambes', 'Quadriceps', [], ['Machine'], 'debutant'), - exo('Presse à cuisses', 'jambes', 'Quadriceps', ['Fessiers'], ['Machine'], 'debutant'), + exo('Leg Press', 'jambes', 'Quadriceps', ['Fessiers'], ['Machine'], 'debutant'), exo('Presse à cuisses inclinée', 'jambes', 'Quadriceps', ['Fessiers'], ['Machine'], 'debutant'), exo('Fentes marchées', 'jambes', 'Quadriceps', ['Fessiers', 'Ischios'], ['Haltères'], 'intermediaire'), exo('Fentes inversées', 'jambes', 'Quadriceps', ['Fessiers'], ['Haltères'], 'debutant'), @@ -348,7 +348,6 @@ export const BUILTIN_CATALOG = [ exo('Around the world plank', 'abdos', 'Transverse', ['Obliques'], ['Poids du corps'], 'intermediaire'), // ─── Variantes complémentaires ────────────────────────────────────────── (+12) - exo('Pec deck inversé', 'epaules', 'Deltoïde postérieur', ['Trapèzes'], ['Machine'], 'debutant'), exo('Développé poitrine machine convergente', 'pectoraux', 'Pectoraux milieu', ['Triceps'], ['Machine'], 'debutant'), exo('Pull-down poignée corde', 'dos', 'Grand dorsal', ['Biceps'], ['Câble'], 'debutant'), exo('Tractions assistées machine', 'dos', 'Grand dorsal', ['Biceps'], ['Machine'], 'debutant'), @@ -445,7 +444,6 @@ export const BUILTIN_CATALOG = [ exo('Walking lunge poids du corps', 'jambes', 'Quadriceps', ['Fessiers', 'Ischios'], ['Poids du corps'], 'debutant'), exo('Step-up latéral', 'jambes', 'Fessiers', ['Quadriceps'], ['Haltères'], 'debutant'), exo('Romanian deadlift unilatéral haltère', 'jambes', 'Ischios', ['Fessiers'], ['Haltères'], 'avance'), - exo('Single leg leg press', 'jambes', 'Quadriceps', ['Fessiers'], ['Machine'], 'debutant'), exo('Single leg curl debout', 'jambes', 'Ischios', [], ['Machine'], 'debutant'), exo('Goblet squat plate', 'jambes', 'Quadriceps', ['Fessiers'], ['Poids du corps'], 'debutant'), exo('Cable squat', 'jambes', 'Quadriceps', ['Fessiers'], ['Câble'], 'debutant'), diff --git a/front/src/data/trophyCatalog.js b/front/src/data/trophyCatalog.js index 4d302e6..05f31b3 100644 --- a/front/src/data/trophyCatalog.js +++ b/front/src/data/trophyCatalog.js @@ -264,11 +264,16 @@ export const TROPHY_CATEGORIES = [ { id: 'special', label: 'Spécial', icon: 'gift', color: '#FE7439' }, ]; +// Chaque catégorie (locale + backend via BACKEND_CATEGORY_MAP) doit apparaître +// dans exactement un onglet ci-dessous — pas de doublon, pas de trou — pour que +// "Tous" et la somme des onglets filtrés restent toujours cohérents. export const TROPHY_FILTER_TABS = [ - { id: 'all', label: 'Tous' }, - { id: 'force', label: 'Force', categories: ['force', 'corps'] }, - { id: 'endurance', label: 'Endurance', categories: ['heritage', 'exploration', 'regularite'] }, - { id: 'special', label: 'Spécial', categories: ['secret', 'social', 'special'] }, + { id: 'all', label: 'Tous' }, + { id: 'force', label: 'Force', categories: ['force', 'corps'] }, + { id: 'endurance', label: 'Endurance', categories: ['heritage', 'exploration', 'regularite'] }, + { id: 'social', label: 'Social & Multi', categories: ['social'] }, + { id: 'collection', label: 'Coffres & Raretés', categories: ['collection'] }, + { id: 'special', label: 'Spécial', categories: ['secret', 'special'] }, ]; // ─── Helper: évalue le catalogue avec overrides (godMode console) ───────────── diff --git a/front/src/data/tutorialChapters.js b/front/src/data/tutorialChapters.js index 66255f8..839a35f 100644 --- a/front/src/data/tutorialChapters.js +++ b/front/src/data/tutorialChapters.js @@ -166,7 +166,7 @@ export const TUTORIAL_CHAPTERS = [ { key: 'trophies_filters', title: 'Filtrer par Catégorie', - body: "Les onglets 'Force', 'Endurance' et 'Spécial' filtrent les trophées. Les trophées verrouillés montrent leur condition.", + body: "Fais défiler les onglets 'Force', 'Endurance', 'Social & Multi', 'Coffres & Raretés' et 'Spécial' pour filtrer les trophées. Les trophées verrouillés montrent leur condition.", targetKey: 'trophies_filters', position: 'bottom', scrollY: 0, }, { diff --git a/front/src/data/workoutTemplates.js b/front/src/data/workoutTemplates.js index cf45fad..8592a50 100644 --- a/front/src/data/workoutTemplates.js +++ b/front/src/data/workoutTemplates.js @@ -173,7 +173,7 @@ export const TEMPLATES = [ videoQuery: 'soulevé de terre roumain', }), buildExercise({ - name: 'Presse à cuisses', + name: 'Leg Press', targetMuscle: 'Quadriceps', secondaryMuscles: ['Fessiers'], equipment: ['Machine'], diff --git a/front/src/screens/Profile/TrophyRoomScreen.js b/front/src/screens/Profile/TrophyRoomScreen.js index 0b25209..c907a05 100644 --- a/front/src/screens/Profile/TrophyRoomScreen.js +++ b/front/src/screens/Profile/TrophyRoomScreen.js @@ -226,12 +226,9 @@ export default function TrophyRoomScreen({ navigation }) { const allCategories = useMemo(() => [...TROPHY_CATEGORIES, COLLECTION_CATEGORY], []); - // Rattache "Collection" au filtre "Spécial", sans muter les données source. - const filterTabs = useMemo(() => TROPHY_FILTER_TABS.map((tab) => ( - tab.id === 'special' && tab.categories - ? { ...tab, categories: [...tab.categories, 'collection'] } - : tab - )), []); + // TROPHY_FILTER_TABS couvre déjà nativement "collection" via son propre + // onglet dédié ("Coffres & Raretés") — plus besoin de patch ad-hoc ici. + const filterTabs = TROPHY_FILTER_TABS; const visibleCategories = useMemo(() => { if (activeFilter === 'all') return allCategories; @@ -294,21 +291,27 @@ export default function TrophyRoomScreen({ navigation }) { - {/* Filter tabs */} - - {filterTabs.map((tab) => { - const active = activeFilter === tab.id; - return ( - setFilter(tab.id)} - activeOpacity={0.75} - > - {tab.label} - - ); - })} + {/* Filter tabs — scroll horizontal (6 onglets ne tiennent plus à plat) */} + + + {filterTabs.map((tab) => { + const active = activeFilter === tab.id; + return ( + setFilter(tab.id)} + activeOpacity={0.75} + > + {tab.label} + + ); + })} + { const shouldGoBack = infoModal?.onCloseNav; @@ -214,7 +227,7 @@ export default function ManualWorkoutCreatorScreen({ navigation }) { > - Créer une séance + {editWorkout ? 'Modifier la séance' : 'Créer une séance'} diff --git a/front/src/screens/Workouts/WorkoutBuilderScreen.js b/front/src/screens/Workouts/WorkoutBuilderScreen.js index 2495497..c801424 100644 --- a/front/src/screens/Workouts/WorkoutBuilderScreen.js +++ b/front/src/screens/Workouts/WorkoutBuilderScreen.js @@ -2,6 +2,7 @@ import React, { useState, useMemo, useCallback } from 'react'; import { View, Text, + TextInput, TouchableOpacity, ScrollView, StyleSheet, @@ -19,8 +20,10 @@ import MuscleHierarchyPicker from '../../components/workouts/MuscleHierarchyPick import { useCustomExercises } from '../../context/CustomExercisesContext'; import { useWorkoutInProgress } from '../../context/WorkoutInProgressContext'; import { useSavedWorkouts } from '../../context/SavedWorkoutsContext'; +import { useToast } from '../../context/ToastContext'; import { generateWorkout } from '../../data/exerciseCatalog'; import InfoModal from '../../components/common/InfoModal'; +import SaveWorkoutPromptModal from '../../components/workouts/SaveWorkoutPromptModal'; const DURATIONS = [ { id: 30, label: '30 min' }, @@ -30,18 +33,41 @@ const DURATIONS = [ { id: 90, label: '90 min' }, ]; -export default function WorkoutBuilderScreen({ navigation }) { +const DEFAULT_WORKOUT_NAME = 'Séance personnalisée'; + +export default function WorkoutBuilderScreen({ navigation, route }) { const { items: customExercises } = useCustomExercises(); const { loadWorkout } = useWorkoutInProgress(); - const { create: createSavedWorkout } = useSavedWorkouts(); + const { create: createSavedWorkout, update: updateSavedWorkout, remove: removeSavedWorkout } = useSavedWorkouts(); + const { showToast } = useToast(); + + // Édition d'une séance existante (venant de WorkoutListScreen → "Modifier") : + // pré-remplit les critères et le libellé, et bascule le toggle Sauver sur + // "déjà sauvegardée" puisqu'on modifie une entrée qui existe déjà. + const editWorkout = route?.params?.editWorkout ?? null; + const editCriteria = editWorkout?.criteria ?? null; - const [subMuscles, setSubMuscles] = useState([]); - const [equipment, setEquipment] = useState([]); - const [level, setLevel] = useState(''); - const [duration, setDuration] = useState(60); + const [subMuscles, setSubMuscles] = useState(editCriteria?.subMuscles ?? []); + const [equipment, setEquipment] = useState(editCriteria?.equipment ?? []); + const [level, setLevel] = useState(editCriteria?.level ?? ''); + const [duration, setDuration] = useState(editCriteria?.durationMin ?? 60); const [regenKey, setRegenKey] = useState(0); const [saving, setSaving] = useState(false); const [infoModal, setInfoModal] = useState(null); // { title, body } + const [savePromptVisible, setSavePromptVisible] = useState(false); + const [workoutName, setWorkoutName] = useState(editWorkout?.name || DEFAULT_WORKOUT_NAME); + // Non-null ⇔ la séance actuellement en aperçu correspond exactement à une + // entrée déjà sauvegardée (icône "Sauver" pleine). Invalidé dès que les + // critères ou l'aperçu changent, puisque le contenu ne correspond plus. + const [savedWorkoutId, setSavedWorkoutId] = useState(editWorkout?.id ?? null); + // Id de l'entrée à METTRE À JOUR (plutôt que dupliquer) à la prochaine + // sauvegarde — distinct de savedWorkoutId : reste valide même après un + // changement de critères (l'entrée d'origine existe toujours en storage), + // et n'est effacé que si cette entrée est explicitement supprimée (toggle + // off), pour ne jamais tenter de mettre à jour un id déjà supprimé. + const [editingId, setEditingId] = useState(editWorkout?.id ?? null); + + const invalidateSaved = useCallback(() => setSavedWorkoutId(null), []); const toggleArr = useCallback((arr, value) => ( arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value] @@ -62,33 +88,93 @@ export default function WorkoutBuilderScreen({ navigation }) { const onRegenerate = useCallback(() => { setRegenKey((k) => k + 1); - }, []); + invalidateSaved(); + }, [invalidateSaved]); - const onLaunch = useCallback(() => { + // Lance réellement le chrono (navigation vers l'écran de séance active). + const launchWorkout = useCallback(() => { if (!preview || preview.exercises.length === 0) return; loadWorkout(preview); if (navigation) navigation.navigate('Workout', { workout: preview }); }, [preview, loadWorkout, navigation]); - const onSave = useCallback(async () => { + // Bouton "Lancer la séance" : si l'aperçu courant est déjà sauvegardé (toggle + // actif), on lance directement — inutile de redemander. Sinon on intercepte + // pour proposer la sauvegarde avant d'ouvrir le chrono. + const onLaunch = useCallback(() => { + if (!preview || preview.exercises.length === 0) return; + if (savedWorkoutId) { launchWorkout(); return; } + setSavePromptVisible(true); + }, [preview, savedWorkoutId, launchWorkout]); + + // Sauvegarde (création OU mise à jour si editingId pointe vers une entrée + // existante) l'aperçu courant sous workoutName, avec ses critères de + // génération pour permettre une future modification pré-remplie. + const persistCurrentPreview = useCallback(async () => { + const trimmedName = workoutName.trim() || DEFAULT_WORKOUT_NAME; + const payload = { + name: trimmedName, + description: preview.description, + exercises: preview.exercises, + criteria: { subMuscles, equipment, level, durationMin: duration }, + }; + if (editingId) { + return updateSavedWorkout(editingId, payload); + } + return createSavedWorkout(payload); + }, [workoutName, preview, subMuscles, equipment, level, duration, editingId, createSavedWorkout, updateSavedWorkout]); + + // Bouton "Sauver" du bandeau d'aperçu — toggle : sauvegarde si absent, + // retire des séances si déjà sauvegardé (l'icône passe pleine ↔ contour). + const onToggleSave = useCallback(async () => { + if (!preview || preview.exercises.length === 0) return; + setSaving(true); + try { + if (savedWorkoutId) { + await removeSavedWorkout(savedWorkoutId); + setSavedWorkoutId(null); + setEditingId(null); + showToast('Retirée de tes séances', 'success'); + } else { + const item = await persistCurrentPreview(); + setSavedWorkoutId(item.id); + setEditingId(item.id); + showToast('Séance sauvegardée', 'success'); + } + } catch (e) { + setInfoModal({ title: 'Erreur', body: e && e.message ? e.message : 'Action impossible' }); + } finally { + setSaving(false); + } + }, [preview, savedWorkoutId, persistCurrentPreview, removeSavedWorkout, showToast]); + + const handleSaveAndLaunch = useCallback(async () => { if (!preview || preview.exercises.length === 0) return; setSaving(true); try { - await createSavedWorkout({ - name: preview.name, - description: preview.description, - exercises: preview.exercises, - }); - setInfoModal({ - title: 'Sauvegardé', - body: `"${preview.name}" est dans tes séances. Retrouve-la dans la page Séances.`, - }); + const item = await persistCurrentPreview(); + setSavedWorkoutId(item.id); + setEditingId(item.id); } catch (e) { - setInfoModal({ title: 'Erreur', body: e && e.message ? e.message : 'Sauvegarde impossible' }); + // La séance se lance quand même : un échec de sauvegarde ne doit jamais + // empêcher l'entraînement. On informe juste via un toast non bloquant + // (l'utilisateur pourra retenter avec le bouton "Sauver" dédié). + showToast(e && e.message ? e.message : 'Sauvegarde impossible, séance lancée sans template.', 'error'); } finally { setSaving(false); + setSavePromptVisible(false); + launchWorkout(); } - }, [preview, createSavedWorkout]); + }, [preview, persistCurrentPreview, launchWorkout, showToast]); + + const handleLaunchWithoutSave = useCallback(() => { + setSavePromptVisible(false); + launchWorkout(); + }, [launchWorkout]); + + const handleDismissSavePrompt = useCallback(() => { + setSavePromptVisible(false); + }, []); const totalSelectedCount = subMuscles.length; @@ -101,7 +187,7 @@ export default function WorkoutBuilderScreen({ navigation }) { > - Crée ta séance + {editWorkout ? 'Modifier ta séance' : 'Crée ta séance'} @@ -119,7 +205,7 @@ export default function WorkoutBuilderScreen({ navigation }) { { setSubMuscles(v); invalidateSaved(); }} showSelectAll /> @@ -131,7 +217,7 @@ export default function WorkoutBuilderScreen({ navigation }) { key={eq.id} label={eq.label} selected={equipment.includes(eq.label)} - onPress={() => setEquipment((s) => toggleArr(s, eq.label))} + onPress={() => { setEquipment((s) => toggleArr(s, eq.label)); invalidateSaved(); }} /> ))} @@ -144,7 +230,7 @@ export default function WorkoutBuilderScreen({ navigation }) { key={lv.id} label={lv.label} selected={level === lv.id} - onPress={() => setLevel(level === lv.id ? '' : lv.id)} + onPress={() => { setLevel(level === lv.id ? '' : lv.id); invalidateSaved(); }} /> ))} @@ -157,12 +243,24 @@ export default function WorkoutBuilderScreen({ navigation }) { key={d.id} label={d.label} selected={duration === d.id} - onPress={() => setDuration(d.id)} + onPress={() => { setDuration(d.id); invalidateSaved(); }} /> ))} +
+ +
+ @@ -176,14 +274,24 @@ export default function WorkoutBuilderScreen({ navigation }) { {preview && preview.exercises.length > 0 ? ( - - {saving ? '...' : 'Sauver'} + + + {saving ? '...' : savedWorkoutId ? 'Sauvée' : 'Sauver'} + setInfoModal(null)} /> + + ); } @@ -361,12 +477,28 @@ const styles = StyleSheet.create({ marginLeft: 6, }, previewActionDisabled: { opacity: 0.5 }, + previewActionSaved: { + backgroundColor: Colors.primary, + borderColor: Colors.primary, + }, previewActionText: { color: Colors.primary, fontSize: 12, fontWeight: '700', marginLeft: 4, }, + previewActionTextSaved: { color: '#fff' }, + nameInput: { + backgroundColor: Colors.card, + borderRadius: 12, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.08)', + paddingHorizontal: 14, + paddingVertical: 12, + color: Colors.textPrimary, + fontSize: 14, + fontWeight: '600', + }, previewEmpty: { paddingVertical: 18, alignItems: 'center', @@ -400,6 +532,7 @@ const styles = StyleSheet.create({ fontWeight: '600', }, previewItemMuscle: { + color: Colors.textSecondary, fontSize: 12, marginTop: 2, }, diff --git a/front/src/screens/Workouts/WorkoutListScreen.js b/front/src/screens/Workouts/WorkoutListScreen.js index b7c0b04..92c8051 100644 --- a/front/src/screens/Workouts/WorkoutListScreen.js +++ b/front/src/screens/Workouts/WorkoutListScreen.js @@ -21,6 +21,7 @@ import { useTutorial, useTutorialTarget } from '../../context/TutorialContext'; import MultiLobbyModal from '../../components/workouts/MultiLobbyModal'; import ConfirmModal from '../../components/common/ConfirmModal'; import InfoModal from '../../components/common/InfoModal'; +import ActionSheetModal from '../../components/common/ActionSheetModal'; // Page d'entrée "Séances". // Header : titre + 2 icônes (Mes exercices, Créer un exercice). @@ -115,6 +116,7 @@ export default function WorkoutListScreen({ navigation, route }) { }, [route?.params?.pendingLobbyId, navigation]); const [deleteSavedTarget, setDeleteSavedTarget] = useState(null); + const [actionSheetTarget, setActionSheetTarget] = useState(null); const [errorInfo, setErrorInfo] = useState(null); // ─── Tutorial ───────────────────────────────────────────────────────────── @@ -207,7 +209,19 @@ export default function WorkoutListScreen({ navigation, route }) { setConfirmItem(null); }, [confirmItem, navigation, loadWorkout]); - const onLongPressSaved = useCallback((saved) => setDeleteSavedTarget(saved), []); + const onLongPressSaved = useCallback((saved) => setActionSheetTarget(saved), []); + + // "Modifier" route vers l'écran de création d'ORIGINE de la séance, pour que + // l'interface d'édition soit rigoureusement identique à celle de création : + // WorkoutBuilder pour toute séance sur-mesure (isManual: false), même une + // séance ancienne créée avant l'ajout du champ criteria (elle rouvrira alors + // l'écran sur-mesure avec des critères vierges plutôt qu'un éditeur différent) ; + // ManualWorkoutCreator uniquement pour les séances explicitement manuelles. + const onEditSaved = useCallback((saved) => { + if (!navigation) return; + const target = saved.isManual ? 'ManualWorkoutCreator' : 'WorkoutBuilder'; + navigation.navigate(target, { editWorkout: saved }); + }, [navigation]); const confirmDeleteSaved = useCallback(async () => { const saved = deleteSavedTarget; @@ -389,6 +403,16 @@ export default function WorkoutListScreen({ navigation, route }) { onReady={handleMultiReady} /> + onEditSaved(actionSheetTarget) }, + { label: 'Supprimer', destructive: true, onPress: () => setDeleteSavedTarget(actionSheetTarget) }, + ]} + onClose={() => setActionSheetTarget(null)} + /> +