166 lines
5.1 KiB
Dart
166 lines
5.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../core/i18n/app_locale.dart';
|
|
import '../../core/theme/tokens.dart';
|
|
import 'campaign_data.dart';
|
|
import 'widgets/campaign_rank.dart';
|
|
import 'widgets/campaign_style.dart';
|
|
|
|
/// 기여 랭킹 더보기 — 화면 대조 원장 F5 · Figma `333:70971`.
|
|
///
|
|
/// 명세 `536:14361` — *내 순위 · 순위 변동값 · 프로필 · 닉네임 · 기여 POP*
|
|
/// 명세 `536:14371` — *기여 POP에 따른 랭킹 소팅 · 실시간 데이터 기준*
|
|
/// 명세 `536:14382` — *랭킹 필터 바텀시트 — 실시간 · 일간 · 주간*
|
|
class CampaignRankingScreen extends StatefulWidget {
|
|
CampaignRankingScreen({
|
|
super.key,
|
|
required this.campaignId,
|
|
CampaignRepository? repository,
|
|
}) : repository = repository ?? SampleCampaignRepository();
|
|
|
|
final String campaignId;
|
|
final CampaignRepository repository;
|
|
|
|
@override
|
|
State<CampaignRankingScreen> createState() => _CampaignRankingScreenState();
|
|
}
|
|
|
|
class _CampaignRankingScreenState extends State<CampaignRankingScreen> {
|
|
List<ContributorRank> _ranks = const [];
|
|
RankWindow _window = RankWindow.live;
|
|
bool _loading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
setState(() => _loading = true);
|
|
final ranks = await widget.repository.ranking(
|
|
widget.campaignId,
|
|
window: _window,
|
|
);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_ranks = ranks;
|
|
_loading = false;
|
|
});
|
|
}
|
|
|
|
ContributorRank? get _me {
|
|
for (final r in _ranks) {
|
|
if (r.isMe) return r;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final me = _me;
|
|
final sorted = [..._ranks]..sort((a, b) => a.rank.compareTo(b.rank));
|
|
final top = sorted.take(3).toList();
|
|
final rest = sorted.skip(3).toList();
|
|
// 시안(333:70971) — 「USER RANKING」 · 나의 기여 랭킹 카드 · 「실시간 랭킹 ⌄」 필터 · 단상 · 목록.
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('USER RANKING')),
|
|
body: _loading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: ListView(
|
|
padding: const EdgeInsets.all(AppSpace.s4),
|
|
children: [
|
|
// 내 순위를 맨 위에 고정한다. 목록을 훑지 않아도 보여야 한다.
|
|
if (me != null) ...[
|
|
Text(
|
|
context.l10n.campaignMyRanking,
|
|
style: campaignText(24, 500, AppColors.onCosmic, height: 1),
|
|
),
|
|
const SizedBox(height: AppSpace.s4),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF19191C),
|
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
|
),
|
|
child: RankRow(
|
|
rank: me,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpace.s5),
|
|
],
|
|
CampaignRankHeader(window: _window, onTap: _pickWindow),
|
|
const SizedBox(height: AppSpace.s3),
|
|
RankPodium(top: top),
|
|
const SizedBox(height: AppSpace.s3),
|
|
for (final r in rest) RankRow(rank: r),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _pickWindow() async {
|
|
final picked = await showModalBottomSheet<RankWindow>(
|
|
context: context,
|
|
builder: (context) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
for (final w in RankWindow.values)
|
|
ListTile(
|
|
title: Text(w.label),
|
|
trailing: _window == w ? const Icon(Icons.check) : null,
|
|
onTap: () => Navigator.of(context).pop(w),
|
|
),
|
|
ListTile(
|
|
title: Text(context.l10n.commonCancel),
|
|
onTap: () => Navigator.of(context).pop(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
if (picked == null || !mounted) return;
|
|
setState(() => _window = picked);
|
|
await _load();
|
|
}
|
|
}
|
|
|
|
/// 「실시간 랭킹 ⌄」 — 18/600 · 아래 꺾쇠 20. 누르면 구간 바텀시트(명세 `536:14382`).
|
|
class CampaignRankHeader extends StatelessWidget {
|
|
const CampaignRankHeader({
|
|
super.key,
|
|
required this.window,
|
|
required this.onTap,
|
|
});
|
|
|
|
final RankWindow window;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
context.l10n.campaignWindowRanking(window.label),
|
|
style: campaignText(18, 600, AppColors.onCosmic, spacing: 0),
|
|
),
|
|
const SizedBox(width: 4),
|
|
const ImageIcon(
|
|
AssetImage('assets/figma/campaign-chevron-down.png'),
|
|
size: 20,
|
|
color: AppColors.onCosmic,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|