Files
fanuzu/app/lib/features/home/widgets/quest_row.dart
T

240 lines
8.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import '../../../core/theme/tokens.dart';
import '../home_data.dart';
import '../home_screen.dart' show formatPop;
import '../../../core/i18n/app_locale.dart';
import '../../quest/widgets/time_attack_countdown.dart';
/// TODAY'S QUEST 한 줄 — Figma 「오늘의 출석체크 / 출석 완료 / +{보상} / 보상받기」.
///
/// 보상 숫자는 Figma 의 50 이 아니라 서버 지급액과 같은 원천에서 온다.
///
/// 상태별 오른쪽 버튼이 다르다. 정책 설계서 §P-05 — 조건 미충족 시 보상은
/// 지급하지 않는다(`E-QST-01`).
class QuestRow extends StatelessWidget {
const QuestRow({super.key, required this.quest, this.onTap});
final QuestSummary quest;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
return Material(
color: AppColors.cosmicSurfaceContainer,
borderRadius: BorderRadius.circular(AppRadius.md),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.md),
child: Padding(
padding: const EdgeInsets.all(AppSpace.s4),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(quest.title, style: t.bodyMedium),
const SizedBox(height: AppSpace.s1),
Text(_subtitle(quest), style: t.bodySmall),
],
),
),
const SizedBox(width: AppSpace.s3),
Text(
_reward(quest),
style: t.titleMedium?.copyWith(color: AppColors.cosmicAccent),
),
const SizedBox(width: AppSpace.s3),
_Action(state: quest.state, onTap: onTap),
],
),
),
),
);
}
static String _subtitle(QuestSummary q) => switch (q.state) {
QuestState.claimable => appL10n.questAttended,
QuestState.inProgress => q.progressLabel ?? appL10n.questInProgressSpaced,
QuestState.done => appL10n.questRewardPaid,
};
}
class _Action extends StatelessWidget {
const _Action({required this.state, this.onTap});
final QuestState state;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final t = Theme.of(context).textTheme;
return switch (state) {
QuestState.claimable => FilledButton(
onPressed: onTap,
style: FilledButton.styleFrom(
minimumSize: const Size(0, AppSizing.touchMinApp),
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4),
backgroundColor: AppColors.cosmicAccent,
foregroundColor: AppColors.onCosmicAccent,
),
child: Text(_claimLabel),
),
QuestState.inProgress => OutlinedButton(
onPressed: onTap,
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, AppSizing.touchMinApp),
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s4),
foregroundColor: AppColors.onCosmic,
side: const BorderSide(color: AppColors.cosmicOutline),
),
child: Text(_startLabel),
),
QuestState.done => Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpace.s2),
child: Text(_doneLabel, style: t.bodySmall),
),
};
}
}
/// 사본 HOME 「TODAY'S QUEST」 버튼 문구(`252:159354` spec). 사본 HOME 에 없는 완료 상태는
/// 퀘스트 화면(C 그룹)의 「보상 완료」와 맞춘다.
String get _claimLabel => appL10n.questClaim;
String get _startLabel => appL10n.questStart;
String get _doneLabel => appL10n.questRewardDone;
/// 홈 「TODAY'S QUEST」 가로 카드 — 사본 `Frame 1984081083`(196×137 · #1A1A1C · r12 ·
/// 패딩 12 · 간격 28): 제목 16 · 상태 14 #A3A2A0 · 오른쪽 「+보상」 20 #2AC28B, 아래 37 버튼
/// (보상 가능 = 초록 #3EB58A 흰 글자 · 진행 중 = 흰 바탕 검정 글자). 누르면 퀘스트 목록으로 간다.
class QuestCard extends StatelessWidget {
const QuestCard({super.key, required this.quest, this.onTap});
final QuestSummary quest;
final VoidCallback? onTap;
static const width = 196.0;
@override
Widget build(BuildContext context) {
final (label, fill, ink) = switch (quest.state) {
QuestState.claimable => (
_claimLabel,
const Color(0xFF3EB58A),
AppColors.onCosmic,
),
QuestState.inProgress => (_startLabel, AppColors.onCosmic, Colors.black),
QuestState.done => (
_doneLabel,
AppColors.cosmicSurfaceRaised,
AppColors.onCosmicMuted,
),
};
return Material(
color: AppColors.cosmicSurfaceContainer,
borderRadius: BorderRadius.circular(AppRadius.md),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.md),
child: Padding(
padding: const EdgeInsets.all(AppSpace.s3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
quest.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.4,
color: AppColors.onCosmic,
),
),
const SizedBox(height: 6),
// 타임어택은 받기 전까지 남은 시간을 센다(FR-QST-06).
if (quest.endsAt case final endsAt?
when quest.state != QuestState.done)
TimeAttackCountdown(
endsAt: endsAt,
builder: (context, left) => _subtitleText(
left == null
? context.l10n.questTimeAttackClosed
: context.l10n.questTimeAttackLeft(
formatLeft(left),
),
),
)
else
_subtitleText(QuestRow._subtitle(quest)),
],
),
),
const SizedBox(width: AppSpace.s2),
Text(
_reward(quest),
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 20,
fontWeight: FontWeight.w600,
height: 1,
color: Color(0xFF2AC28B),
),
),
],
),
Container(
height: 37,
alignment: Alignment.center,
decoration: BoxDecoration(
color: fill,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
label,
style: TextStyle(
fontFamily: AppFonts.body,
fontSize: 14,
fontWeight: FontWeight.w600,
height: 1,
color: ink,
),
),
),
],
),
),
),
);
}
}
Widget _subtitleText(String text) => Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 14,
fontWeight: FontWeight.w600,
height: 1.4,
color: AppColors.onCosmicVariant,
),
);
/// 운영값을 못 읽었으면 숫자를 지어내지 않는다.
String _reward(QuestSummary quest) =>
quest.rewardPop == null ? 'POP' : '+${formatPop(quest.rewardPop!)}';