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' },