diff --git a/back/controllers/groupStreak.controller.js b/back/controllers/groupStreak.controller.js index b185c80..a2f43c5 100644 --- a/back/controllers/groupStreak.controller.js +++ b/back/controllers/groupStreak.controller.js @@ -53,10 +53,11 @@ function startOfToday() { /** * XP d'amitié attribué par validation de streak de groupe. - * Palier : +10 XP de base, +10 tous les 7 jours de streak consécutive. + * Palier : +5 XP de base, +5 tous les 30 jours de streak consécutive + * (environ 4 mois de streak ininterrompue pour atteindre le niveau 5 maximum). */ function computeGroupFriendshipXpGain(currentStreak) { - return 10 * (Math.floor(currentStreak / 7) + 1); + return 5 * (Math.floor((currentStreak - 1) / 30) + 1); } /** diff --git a/front/src/components/social/FriendshipLevelUpModal.js b/front/src/components/social/FriendshipLevelUpModal.js index 67cd3ef..d52ba91 100644 --- a/front/src/components/social/FriendshipLevelUpModal.js +++ b/front/src/components/social/FriendshipLevelUpModal.js @@ -3,6 +3,7 @@ import { View, Text, StyleSheet, Modal, TouchableOpacity, Animated } from 'react import { Ionicons } from '@expo/vector-icons'; import { Colors } from '../../constants/theme'; import BirthdayConfetti from '../profile/BirthdayConfetti'; +import { getFriendshipTitle } from '../../data/friendshipTitles'; // Progression de couleur vers le Rouge Sang du niveau 5 ("Lien de Sang") — // même esprit que RANK_COLORS dans LevelUpModal.js, mais indexé par niveau @@ -29,6 +30,7 @@ const FRIENDSHIP_LEVEL_COLORS = { export default function FriendshipLevelUpModal({ visible, pseudo, level, onClose }) { const color = FRIENDSHIP_LEVEL_COLORS[level] || FRIENDSHIP_LEVEL_COLORS[1]; const isMax = level >= 5; + const friendshipTitle = getFriendshipTitle(level); const badgeScale = useRef(new Animated.Value(0)).current; useEffect(() => { @@ -56,6 +58,7 @@ export default function FriendshipLevelUpModal({ visible, pseudo, level, onClose NIVEAU D'AMITIÉ Niveau {level}/5 + {friendshipTitle.label} avec {pseudo} @@ -118,6 +121,11 @@ const styles = StyleSheet.create({ fontWeight: '800', letterSpacing: -0.5, }, + title: { + fontSize: 13, + fontWeight: '700', + marginTop: 6, + }, rank: { color: Colors.textSecondary, fontSize: 14, diff --git a/front/src/data/friendshipTitles.js b/front/src/data/friendshipTitles.js new file mode 100644 index 0000000..1618f79 --- /dev/null +++ b/front/src/data/friendshipTitles.js @@ -0,0 +1,18 @@ +// Titres de Relation (Titres d'Amitié) — cosmétique textuel affiché sous le +// niveau d'amitié entre deux amis. Purement déclaratif côté front : le niveau +// d'amitié (1-5) est calculé par le backend (voir groupStreak.controller.js +// → computeFriendshipLevel), ce fichier ne fait que le traduire en libellé. + +export const FRIENDSHIP_TITLES = { + 1: { label: 'Nouveaux Partenaires' }, + 2: { label: 'Assisteurs de Confiance' }, + 3: { label: 'Frères de Fonte' }, + 4: { label: 'Rivaux Légendaires' }, + 5: { label: 'Âmes Sœurs de Muscle' }, +}; + +/** Renvoie { label } pour un niveau d'amitié donné (1-5, clampé). */ +export function getFriendshipTitle(level) { + const clamped = Math.min(5, Math.max(1, Number(level) || 1)); + return FRIENDSHIP_TITLES[clamped]; +} diff --git a/front/src/data/tutorialChapters.js b/front/src/data/tutorialChapters.js index 839a35f..8250797 100644 --- a/front/src/data/tutorialChapters.js +++ b/front/src/data/tutorialChapters.js @@ -259,6 +259,12 @@ export const TUTORIAL_CHAPTERS = [ body: "Si la streak de groupe casse, le dernier Briseur s'affiche dans le Hall of Shame. Le bouton 'Secouer' envoie une notif troll aux retardataires pour les motiver.", targetKey: null, position: 'center', scrollY: null, }, + { + key: 'social_friendship_titles', + title: "Titres d'Amitié", + body: "Chaque jour de streak de groupe validé à plusieurs renforce ton lien avec chaque coéquipier. De 'Nouveaux Partenaires' à 'Âmes Sœurs de Muscle' au niveau 5 maximum, un Titre d'Amitié affiche l'histoire que vous avez construite ensemble.", + targetKey: null, position: 'center', scrollY: null, + }, { key: 'social_multi', title: 'Le Mode Multi', diff --git a/front/src/screens/Social/FriendProfileScreen.js b/front/src/screens/Social/FriendProfileScreen.js index 6e738fb..9eb8ab4 100644 --- a/front/src/screens/Social/FriendProfileScreen.js +++ b/front/src/screens/Social/FriendProfileScreen.js @@ -19,6 +19,7 @@ import AchievementShowcase from '../../components/profile/AchievementShowcase'; import FriendShowcaseGrid from '../../components/profile/FriendShowcaseGrid'; import PersonalRecordsList from '../../components/profile/PersonalRecordsList'; import { resolveExerciseMeta } from '../../data/majorExercises'; +import { getFriendshipTitle } from '../../data/friendshipTitles'; // ─── FriendProfileScreen ────────────────────────────────────────────────────── // Profil public d'un ami (Brique III) : miroir en lecture seule de notre propre @@ -111,6 +112,7 @@ export default function FriendProfileScreen({ route, navigation }) { const level = useMemo(() => xpToLevel(profile?.user?.xp ?? 0).level, [profile]); const rank = useMemo(() => getRank(level), [level]); + const friendshipTitle = useMemo(() => getFriendshipTitle(profile?.friendshipLevel), [profile]); const isElite = level >= 91; const isLegend = level >= 171; const isGod = level >= 200; @@ -205,6 +207,7 @@ export default function FriendProfileScreen({ route, navigation }) { Niveau d'amitié {profile.friendshipLevel}/5 {profile.friendshipLevel === 5 ? ' · Lien de Sang' : ''} + {friendshipTitle.label} {profile.friendshipXp} XP d'amitié @@ -293,6 +296,7 @@ const styles = StyleSheet.create({ }, friendshipHearts: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 }, friendshipLevel: { color: Colors.textPrimary, fontSize: 13.5, fontWeight: '700' }, + friendshipTitle: { color: Colors.primary, fontSize: 12, fontWeight: '700', marginTop: 4 }, friendshipXp: { color: Colors.textMuted, fontSize: 11.5, marginTop: 2 }, shakeBanner: { diff --git a/front/src/screens/Social/SocialScreen.js b/front/src/screens/Social/SocialScreen.js index ac12fd4..60fbe5b 100644 --- a/front/src/screens/Social/SocialScreen.js +++ b/front/src/screens/Social/SocialScreen.js @@ -20,6 +20,7 @@ import { getMyGroup, inviteToGroup, respondToGroupInvite, shakeMember, checkGroupStreak, leaveGroup, } from '../../services'; import { MAJOR_EXERCISES } from '../../data/majorExercises'; +import { getFriendshipTitle } from '../../data/friendshipTitles'; import ExercisePickerModal from '../../components/social/ExercisePickerModal'; import TutorialOverlay from '../../components/tutorial/TutorialOverlay'; import { useTutorial, useTutorialTarget } from '../../context/TutorialContext'; @@ -419,7 +420,7 @@ function FriendsSegment({ ) : friends.map((f, i) => ( onOpenProfile(f.user, f.friendshipLevel)}> - + onRemoveFriend(f.friendshipId, f.user.pseudo)} @@ -885,7 +886,7 @@ const WEATHER_META = { sleeping: { icon: 'moon', color: Colors.textMuted, label: 'En sommeil' }, }; -function UserRow({ user, children }) { +function UserRow({ user, subtitle, children }) { const weather = user?.weatherStatus ? WEATHER_META[user.weatherStatus] : null; return ( @@ -903,6 +904,7 @@ function UserRow({ user, children }) { Nv. {user?.level ?? 1} · {user?.rank ?? 'Novice'} {weather ? ` · ${weather.label}` : ''} + {subtitle ? {subtitle} : null} {children} @@ -1007,6 +1009,7 @@ const styles = StyleSheet.create({ }, userPseudo: { color: Colors.textPrimary, fontSize: 14, fontWeight: '700' }, userMeta: { color: Colors.textMuted, fontSize: 11.5, marginTop: 1 }, + userFriendshipTitle: { color: Colors.primary, fontSize: 10.5, fontWeight: '700', marginTop: 2 }, userActions:{ flexDirection: 'row', alignItems: 'center', gap: 6 }, hearts: { flexDirection: 'row', alignItems: 'center' },