Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions back/controllers/groupStreak.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
8 changes: 8 additions & 0 deletions front/src/components/social/FriendshipLevelUpModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(() => {
Expand Down Expand Up @@ -56,6 +58,7 @@ export default function FriendshipLevelUpModal({ visible, pseudo, level, onClose

<Text style={styles.eyebrow}>NIVEAU D'AMITIÉ</Text>
<Text style={[styles.level, { color }]}>Niveau {level}/5</Text>
<Text style={[styles.title, { color }]}>{friendshipTitle.label}</Text>
<Text style={styles.rank}>avec {pseudo}</Text>

<Text style={styles.body}>
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions front/src/data/friendshipTitles.js
Original file line number Diff line number Diff line change
@@ -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];
}
6 changes: 6 additions & 0 deletions front/src/data/tutorialChapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions front/src/screens/Social/FriendProfileScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -205,6 +207,7 @@ export default function FriendProfileScreen({ route, navigation }) {
Niveau d'amitié {profile.friendshipLevel}/5
{profile.friendshipLevel === 5 ? ' · Lien de Sang' : ''}
</Text>
<Text style={styles.friendshipTitle}>{friendshipTitle.label}</Text>
<Text style={styles.friendshipXp}>{profile.friendshipXp} XP d'amitié</Text>
</View>

Expand Down Expand Up @@ -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: {
Expand Down
7 changes: 5 additions & 2 deletions front/src/screens/Social/SocialScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -419,7 +420,7 @@ function FriendsSegment({
) : friends.map((f, i) => (
<AnimatedRow key={f.user._id} index={i}>
<TouchableOpacity activeOpacity={0.75} onPress={() => onOpenProfile(f.user, f.friendshipLevel)}>
<UserRow user={f.user}>
<UserRow user={f.user} subtitle={getFriendshipTitle(f.friendshipLevel).label}>
<FriendshipHearts level={f.friendshipLevel ?? 1} />
<TouchableOpacity
onPress={() => onRemoveFriend(f.friendshipId, f.user.pseudo)}
Expand Down Expand Up @@ -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 (
<View style={styles.userRow}>
Expand All @@ -903,6 +904,7 @@ function UserRow({ user, children }) {
Nv. {user?.level ?? 1} · {user?.rank ?? 'Novice'}
{weather ? ` · ${weather.label}` : ''}
</Text>
{subtitle ? <Text style={styles.userFriendshipTitle}>{subtitle}</Text> : null}
</View>
<View style={styles.userActions}>{children}</View>
</View>
Expand Down Expand Up @@ -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' },

Expand Down
Loading