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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5954510b93
commit
f5e3075205
@@ -0,0 +1,262 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user