- Wallet EnakPoint & EnakCoin: beranda, riwayat, saldo kedaluwarsa, kode bayar, tukar EnakCoin, transfer - PIN (buat, ganti, lupa), outlet, riwayat order, game spin - Halaman community, webview, coming soon, dan ikon menu beranda baru - Redesign Reward Saya: kartu tiket, filter chip, detail kode + QR - Hapus customer point loader, menu, dan checkout lama - Base URL ke enaklo-pos-api.altru.id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
263 lines
7.5 KiB
Dart
263 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
import '../../../common/theme/theme.dart';
|
|
import '../../../sample/community_sample_data.dart';
|
|
|
|
/// Palet lembut khusus EnakCommunity.
|
|
class CommunityColor {
|
|
static const Color background = Color(0xFFFAF7F5);
|
|
static const Color blush = Color(0xFFFFEEEC);
|
|
static const Color blushDeep = Color(0xFFFFDCD7);
|
|
static const Color divider = Color(0xFFF1ECE9);
|
|
static const Color muted = Color(0xFF9A918D);
|
|
|
|
/// Pasangan (latar, aksen) untuk komunitas & avatar.
|
|
static const List<(Color, Color)> tints = [
|
|
(Color(0xFFFFF1E6), Color(0xFFE07A3F)), // peach
|
|
(Color(0xFFF1ECE4), Color(0xFF8A6A4F)), // latte
|
|
(Color(0xFFEAF3FC), Color(0xFF4A86C5)), // sky
|
|
(Color(0xFFFFEEEC), Color(0xFFD14B4B)), // blush
|
|
(Color(0xFFE9F7EF), Color(0xFF3F9A67)), // mint
|
|
(Color(0xFFF1EDFB), Color(0xFF7D63C9)), // lavender
|
|
];
|
|
|
|
static (Color, Color) tintOf(int index) => tints[index % tints.length];
|
|
}
|
|
|
|
/// Komunitas yang diikuti, dibagi antar layar. Sementara di memori sampai
|
|
/// API komunitas tersedia.
|
|
final ValueNotifier<Set<String>> joinedCommunityIds = ValueNotifier({
|
|
for (final group in communityGroups)
|
|
if (group.isJoined) group.id,
|
|
});
|
|
|
|
void toggleJoinCommunity(String groupId) {
|
|
final ids = {...joinedCommunityIds.value};
|
|
if (!ids.remove(groupId)) ids.add(groupId);
|
|
joinedCommunityIds.value = ids;
|
|
}
|
|
|
|
/// Bayangan tipis agar kartu tampak "mengambang" tanpa garis tepi.
|
|
const List<BoxShadow> communitySoftShadow = [
|
|
BoxShadow(color: Color(0x0D3B2A24), blurRadius: 24, offset: Offset(0, 8)),
|
|
];
|
|
|
|
BoxDecoration communityCardDecoration({double radius = 20}) => BoxDecoration(
|
|
color: AppColor.white,
|
|
borderRadius: BorderRadius.circular(radius),
|
|
boxShadow: communitySoftShadow,
|
|
);
|
|
|
|
/// "Baru saja", "5 mnt", "3 jam", "2 hari", lalu tanggal pendek.
|
|
String formatTimeAgo(DateTime time) {
|
|
final diff = DateTime.now().difference(time);
|
|
if (diff.inMinutes < 1) return 'Baru saja';
|
|
if (diff.inHours < 1) return '${diff.inMinutes} mnt';
|
|
if (diff.inDays < 1) return '${diff.inHours} jam';
|
|
if (diff.inDays < 7) return '${diff.inDays} hari';
|
|
return DateFormat('d MMM', 'id_ID').format(time);
|
|
}
|
|
|
|
/// 950 → "950", 12480 → "12,4rb".
|
|
String formatCompact(int value) {
|
|
if (value < 1000) return '$value';
|
|
if (value < 1000000) {
|
|
return '${NumberFormat('0.#', 'id_ID').format(value / 1000)}rb';
|
|
}
|
|
return '${NumberFormat('0.#', 'id_ID').format(value / 1000000)}jt';
|
|
}
|
|
|
|
/// Avatar inisial dengan warna lembut yang konsisten per anggota.
|
|
class CommunityAvatar extends StatelessWidget {
|
|
const CommunityAvatar({super.key, required this.member, this.size = 40});
|
|
|
|
final CommunityMember member;
|
|
final double size;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final (background, accent) = CommunityColor.tintOf(
|
|
member.id.codeUnits.fold(0, (sum, c) => sum + c),
|
|
);
|
|
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(color: background, shape: BoxShape.circle),
|
|
child: Text(
|
|
member.initials,
|
|
style: AppStyle.md.copyWith(
|
|
fontSize: size * 0.36,
|
|
color: accent,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Deretan avatar bertumpuk, mis. peserta event.
|
|
class CommunityAvatarStack extends StatelessWidget {
|
|
const CommunityAvatarStack({
|
|
super.key,
|
|
required this.members,
|
|
this.size = 26,
|
|
this.max = 3,
|
|
});
|
|
|
|
final List<CommunityMember> members;
|
|
final double size;
|
|
final int max;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final shown = members.take(max).toList();
|
|
final overlap = size * 0.65;
|
|
|
|
return SizedBox(
|
|
width: size + overlap * (shown.length - 1),
|
|
height: size,
|
|
child: Stack(
|
|
children: [
|
|
for (var i = 0; i < shown.length; i++)
|
|
Positioned(
|
|
left: overlap * i,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(1.5),
|
|
decoration: const BoxDecoration(
|
|
color: AppColor.white,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: CommunityAvatar(member: shown[i], size: size - 3),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Emoji komunitas di atas kotak berwarna lembut.
|
|
class CommunityEmojiBadge extends StatelessWidget {
|
|
const CommunityEmojiBadge({super.key, required this.group, this.size = 44});
|
|
|
|
final CommunityGroup group;
|
|
final double size;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final (background, _) = CommunityColor.tintOf(group.tint);
|
|
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: background,
|
|
borderRadius: BorderRadius.circular(size * 0.32),
|
|
),
|
|
child: Text(group.emoji, style: TextStyle(fontSize: size * 0.48)),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Tombol pil "Gabung" / "Tergabung".
|
|
class CommunityJoinButton extends StatelessWidget {
|
|
const CommunityJoinButton({
|
|
super.key,
|
|
required this.isJoined,
|
|
required this.onPressed,
|
|
this.compact = false,
|
|
});
|
|
|
|
final bool isJoined;
|
|
final VoidCallback onPressed;
|
|
final bool compact;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
decoration: BoxDecoration(
|
|
color: isJoined ? CommunityColor.blush : AppColor.primary,
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(999),
|
|
onTap: onPressed,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: compact ? 12 : 16,
|
|
vertical: compact ? 6 : 8,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
isJoined ? Icons.check_rounded : Icons.add_rounded,
|
|
size: compact ? 14 : 16,
|
|
color: isJoined ? AppColor.primary : AppColor.white,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
isJoined ? 'Tergabung' : 'Gabung',
|
|
style: (compact ? AppStyle.xs : AppStyle.sm).copyWith(
|
|
color: isJoined ? AppColor.primary : AppColor.white,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Judul seksi dengan aksi opsional di kanan.
|
|
class CommunitySectionTitle extends StatelessWidget {
|
|
const CommunitySectionTitle({
|
|
super.key,
|
|
required this.title,
|
|
this.actionLabel,
|
|
this.onAction,
|
|
});
|
|
|
|
final String title;
|
|
final String? actionLabel;
|
|
final VoidCallback? onAction;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 20, 12, 10),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
if (actionLabel != null)
|
|
TextButton(
|
|
onPressed: onAction,
|
|
child: Text(
|
|
actionLabel!,
|
|
style: AppStyle.sm.copyWith(
|
|
color: AppColor.primary,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|