Files
enaklo-flutter/lib/presentation/pages/community/community_ui.dart
T
efrilmandClaude Opus 5.5 f5e3075205 EnakPoint & EnakCoin wallet, PIN, outlet, order, community, reward
- 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>
2026-10-01 01:43:22 +07:00

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,
),
),
),
],
),
);
}
}