// league.jsx — Leaderboard / Health Behavior Score (League Points)
const { useState: useStateL } = React;
function League({ go, data }) {
const [tab, setTab] = useStateL('league');
const [explain, setExplain] = useStateL(false);
return (
setExplain(true)} style={{ width: 42, height: 42, borderRadius: 999, background: T.bg, border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: `inset 0 0 0 1px ${T.line}` }}>
?
}/>
{/* safety banner */}
건강 행동만 비교해요. 체중·칼로리·외모는 순위에 쓰지 않아요.
{/* segmented */}
{[['league', 'League'], ['friends', 'Friends'], ['squad', 'Squad']].map(s => (
))}
{tab === 'league' && setExplain(true)}/>}
{tab === 'friends' && }
{tab === 'squad' && }
setExplain(false)}/>
);
}
const STANDINGS = [
{ rank: 1, name: 'Minji', pts: 420, days: 5, me: false },
{ rank: 2, name: 'Joon', pts: 401, days: 5 },
{ rank: 3, name: 'Sora', pts: 390, days: 4 },
{ rank: 7, name: 'Hana', pts: 355, days: 4 },
{ rank: 8, name: 'You', pts: 342, days: 4, me: true },
{ rank: 9, name: 'Doyun', pts: 337, days: 3 },
{ rank: 27, name: 'Private User', pts: 120, days: 1, priv: true },
{ rank: 28, name: 'Alex', pts: 115, days: 1 },
];
function Row({ r }) {
return (
{r.rank}
{r.priv ? : (r.me ? '지' : r.name[0])}
{r.name}
{r.days} healthy days
{r.pts}
);
}
function ZoneLabel({ children, tone }) {
return
{children}
;
}
function LeagueView({ data, go, onExplain }) {
const leaderboard = data.leaderboard;
const league = (leaderboard && leaderboard.league) || {};
const standings = (window.FitbodyBackend && window.FitbodyBackend.leaderboardRows(leaderboard)) || STANDINGS;
const pointsToPromotion = league.points_to_promotion || league.pointsToPromotion || 38;
const rank = league.rank || 8;
const size = league.size || 28;
const points = league.points || 342;
return (
{/* tier card */}
{league.name || 'Bronze League'}
{league.week_label || league.weekLabel || 'Week 23'} · {league.remaining_label || league.remainingLabel || '2일 14시간 남음'}
League Points
{/* Ava rec */}
승급권까지 {pointsToPromotion}점 남았어요. 오늘은 무리하지 말고 5분 Mobility Quest면 충분해요.
go('player', { quest: { title: 'Mobility 리셋', seconds: 60, steps: ['천천히 어깨를 돌려요', '옆구리를 늘려요', '깊게 호흡해요'], avaLine: '점수보다 흐름이 중요해요', xp: 18 } })}>추천 Quest 시작
PROMOTION ZONE · 상위 6명 승급
{standings.slice(0, 3).map(r => )}
YOUR ZONE
{standings.slice(3, 6).map(r => )}
MAINTAIN ZONE
{standings.slice(6).map(r => )}
);
}
function FriendsView({ go, data }) {
const friends = (window.FitbodyBackend && window.FitbodyBackend.friendRows(data.leaderboard)) || [
{ rank: 1, name: 'Jimin', pts: 310, days: 4, tag: '🔥 4 healthy days' },
{ rank: 2, name: 'You', pts: 295, days: 3, me: true, tag: '🌱 3 healthy days' },
{ rank: 3, name: 'Sora', pts: 240, days: 2, tag: '🧘 recovery done' },
];
return (
{friends.map(r => )}
친구를 이기는 것보다 흐름을 이어가는 게 목표예요. 3분 Quest 하나로 충분해요.
} onClick={() => go('invite')}>친구 초대하기
);
}
function SquadView({ go, data }) {
const squad = (window.FitbodyBackend && window.FitbodyBackend.squadData(data.leaderboard)) || {};
const goals = squad.goals || [['Healthy days', 21, 28], ['Recovery quests', 5, 6], ['Motion practices', 8, 10]];
const members = squad.members || [['Hana', 'High contribution', T.good], ['You', 'On track', T.coral], ['Min', 'Recovery hero', T.calm], ['Private', 'Participating', T.ink3]];
return (
{squad.name || 'Morning Reset'}
{squad.progress || 78}%
이번 주 팀 목표 달성률
{goals.map((g, i) => (
))}
Contribution
{members.map((m, i) => (
))}
팀 보상까지 2개 Quest 남았어요. 무리한 운동보다 짧은 회복 Quest도 좋아요.
);
}
function ScoreSheet({ open, onClose }) {
const rows = (window.FitbodyBackend && window.FitbodyBackend.scoreRows(window.FitbodyBackend.snapshot().leaderboard)) || [['Tiny Quest 완료', '+130', T.coral], ['꾸준함 보너스', '+62', T.amber], ['Motion Skill 연습', '+40', T.calm], ['Recovery Quest', '+35', T.good], ['검증 보너스', '+25', T.ink2], ['Daily cap 적용', '-18', T.ink3]];
return (
점수는 운동량이 아니라 건강 행동의 균형으로 계산돼요.
{rows.map((r, i) => (
{r[0]}
{r[1]}
))}
이번 주 합계
342
체중·감량·칼로리·운동량 총합은 순위에 절대 쓰지 않아요. Recovery Quest도 점수에 포함돼요.
이해했어요
);
}
Object.assign(window, { League });