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