QUEST · CAMPAIGN 화면 — 퀘스트 참여·생성·인증 · 캠페인 목록·상세·후원·랭킹
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user