Files
fanuzu/app/lib/features/campaign/widgets/campaign_rank.dart
T

234 lines
6.6 KiB
Dart

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<ContributorRank> 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;
}