import 'package:flutter/material.dart'; import '../../../core/theme/tokens.dart'; import '../../../core/ui/relative_time.dart'; import '../campaign_data.dart'; import 'campaign_style.dart'; /// 기여 랭킹 조각 — 시안 `333:70971`(사용자 랭킹) · 캠페인 상세 「실시간 랭킹」 공용. /// 랭킹 데이터에 사진 필드가 없어 시안 목록의 단색 원을 순위 순서대로 돌려 쓴다. const _avatarColors = [ Color(0xFF000000), Color(0xFF7358E9), Color(0xFFFA46E8), Color(0xFFC6D284), Color(0xFFF4CFFA), ]; /// 순위 아바타. 내 줄은 MY 프로필과 같은 시안 표본을 쓴다. class RankAvatar extends StatelessWidget { const RankAvatar({super.key, required this.rank, required this.size}); final ContributorRank rank; final double size; @override Widget build(BuildContext context) => rank.isMe ? ClipOval( child: Image.asset( 'assets/figma/avatar-sample.png', width: size, height: size, fit: BoxFit.cover, ), ) : Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, // 시안 목록은 4위부터 검정 · 보라 · 분홍 · 올리브 · 연분홍 순이다. color: _avatarColors[(rank.rank + 1) % _avatarColors.length], ), ); } /// 순위 숫자 아래 변동값 — 상승 #f28180 ▲ · 하락 #6866f3 ▼ · 변동 없음 「-」. class RankChange extends StatelessWidget { const RankChange({super.key, required this.change}); final int change; @override Widget build(BuildContext context) { if (change == 0) { return Text( '-', style: campaignText(10, 500, const Color(0xFF777777), height: 1), ); } final up = change > 0; final color = up ? const Color(0xFFF28180) : AppColors.neonViolet; return Row( mainAxisSize: MainAxisSize.min, children: [ Text( up ? '▲' : '▼', style: TextStyle(fontSize: 8, height: 1, color: color), ), const SizedBox(width: 2), Text('${change.abs()}', style: campaignText(10, 500, color, height: 1)), ], ); } } /// 순위 한 줄 — 높이 60 · [순위+변동] 28 [아바타 40(원 36) · 6 · 닉네임 14/500 #979b9e] … 기여 POP 16/600. class RankRow extends StatelessWidget { const RankRow({ super.key, required this.rank, this.padding = const EdgeInsets.symmetric(vertical: 10), }); final ContributorRank rank; final EdgeInsets padding; @override Widget build(BuildContext context) => Padding( padding: padding, child: Row( children: [ ConstrainedBox( constraints: const BoxConstraints(minWidth: 17), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '${rank.rank}', style: campaignText(14, 600, AppColors.onCosmic, height: 1), ), const SizedBox(height: 2), RankChange(change: rank.change), ], ), ), const SizedBox(width: 28), // 사본 — 40 프레임 가운데 지름 36 원. SizedBox.square( dimension: 40, child: Center(child: RankAvatar(rank: rank, size: 36)), ), const SizedBox(width: 6), Expanded( child: Text( rank.name, overflow: TextOverflow.ellipsis, style: campaignText(14, 500, const Color(0xFF979B9E), height: 1), ), ), Text( formatReaction(rank.pop), style: campaignText(16, 600, AppColors.onCosmic, height: 1.4), ), ], ), ); } /// 1~3위 단상 — 2 · 1 · 3 순서, 1위만 20 위로 올린다. 왕관 24 · 아바타 80. class RankPodium extends StatelessWidget { const RankPodium({super.key, required this.top}); /// 순위 오름차순 앞 세 줄. final List top; static const _crown = { 1: Color(0xFFFEA300), 2: Color(0xFFACACAC), 3: Color(0xFF9C724A), }; @override Widget build(BuildContext context) { final byRank = {for (final r in top) r.rank: r}; return Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final (i, place) in const [2, 1, 3].indexed) ...[ if (i > 0) const SizedBox(width: 24), SizedBox(width: 81, child: _place(byRank[place], place)), ], ], ); } Widget? _place(ContributorRank? r, int place) => r == null ? null : Padding( padding: EdgeInsets.only(top: place == 1 ? 0 : 20), child: Column( children: [ _Crown(color: _crown[place]!, place: place), RankAvatar(rank: r, size: 80), const SizedBox(height: 8), Text( r.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: campaignText(14, 600, AppColors.onCosmic, height: 1), ), const SizedBox(height: 4), Text( formatReaction(r.pop), style: campaignText(16, 600, AppColors.onCosmic, height: 1.4), ), ], ), ); } /// 체스 킹 왕관(시안 material-symbols:chess-king) — 십자 · 몸통을 그리고 가운데에 순위. class _Crown extends StatelessWidget { const _Crown({required this.color, required this.place}); final Color color; final int place; @override Widget build(BuildContext context) => SizedBox( width: 24, height: 24, child: CustomPaint( painter: _CrownPainter(color), child: Padding( padding: const EdgeInsets.only(top: 10), child: Center( child: Text( '$place', style: campaignText(10, 900, AppColors.onCosmic, height: 1), ), ), ), ), ); } class _CrownPainter extends CustomPainter { const _CrownPainter(this.color); final Color color; @override void paint(Canvas canvas, Size size) { final p = Paint()..color = color; final w = size.width; // 십자 canvas.drawRect(Rect.fromLTWH(w / 2 - 1, 1, 2, 7), p); canvas.drawRect(Rect.fromLTWH(w / 2 - 3.5, 3, 7, 2), p); // 몸통 canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromLTWH(3, 8, w - 6, 15), const Radius.circular(4), ), p, ); } @override bool shouldRepaint(_CrownPainter old) => old.color != color; }