- Bottom navbar: Home, Pesanan, Bayar (tombol tengah ke kode bayar), Community, Profil; menu EnakCommunity di beranda pindah ke tab - Image placeholder: logo Enaklo diperbesar, ilustrasi gelas dihapus - Fix assertion Navigator saat PIN gagal karena error non-PIN: flushbar kini tampil lewat onFailure setelah sheet PIN tertutup (bayar, tukar, transfer) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
511 lines
16 KiB
Dart
511 lines
16 KiB
Dart
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../common/theme/theme.dart';
|
|
import '../../../sample/community_sample_data.dart';
|
|
import '../../components/assets/assets.gen.dart';
|
|
import '../../components/toast/flushbar.dart';
|
|
import '../../router/app_router.gr.dart';
|
|
import 'community_ui.dart';
|
|
import 'widgets/community_compose_sheet.dart';
|
|
import 'widgets/community_event_card.dart';
|
|
import 'widgets/community_group_card.dart';
|
|
import 'widgets/community_post_card.dart';
|
|
|
|
enum _CommunityTab { forYou, groups, events }
|
|
|
|
@RoutePage()
|
|
class CommunityPage extends StatefulWidget {
|
|
const CommunityPage({super.key});
|
|
|
|
@override
|
|
State<CommunityPage> createState() => _CommunityPageState();
|
|
}
|
|
|
|
class _CommunityPageState extends State<CommunityPage> {
|
|
final List<CommunityPost> _posts = [...communityPosts];
|
|
final Set<String> _goingEventIds = {};
|
|
_CommunityTab _tab = _CommunityTab.forYou;
|
|
|
|
void _openGroup(CommunityGroup group) =>
|
|
context.router.push(CommunityDetailRoute(group: group));
|
|
|
|
void _openPost(CommunityPost post) =>
|
|
context.router.push(CommunityPostDetailRoute(post: post));
|
|
|
|
Future<void> _compose() async {
|
|
final joined = communityGroups
|
|
.where((g) => joinedCommunityIds.value.contains(g.id))
|
|
.toList();
|
|
if (joined.isEmpty) {
|
|
AppFlushbar.showError(context, 'Gabung komunitas dulu untuk posting');
|
|
setState(() => _tab = _CommunityTab.groups);
|
|
return;
|
|
}
|
|
|
|
final post = await CommunityComposeSheet.show(context, groups: joined);
|
|
if (post == null || !mounted) return;
|
|
|
|
setState(() {
|
|
_posts.insert(0, post);
|
|
_tab = _CommunityTab.forYou;
|
|
});
|
|
AppFlushbar.showSuccess(context, 'Postingan kamu sudah terbit');
|
|
}
|
|
|
|
void _toggleEvent(CommunityEvent event) {
|
|
final going = !_goingEventIds.contains(event.id);
|
|
setState(() {
|
|
going ? _goingEventIds.add(event.id) : _goingEventIds.remove(event.id);
|
|
});
|
|
if (going) {
|
|
AppFlushbar.showSuccess(context, 'Kamu terdaftar di ${event.title}');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder<Set<String>>(
|
|
valueListenable: joinedCommunityIds,
|
|
builder: (context, joinedIds, _) {
|
|
return Scaffold(
|
|
backgroundColor: CommunityColor.background,
|
|
floatingActionButton: AnimatedScale(
|
|
scale: _tab == _CommunityTab.events ? 0 : 1,
|
|
duration: const Duration(milliseconds: 200),
|
|
child: FloatingActionButton.extended(
|
|
onPressed: _compose,
|
|
elevation: 2,
|
|
backgroundColor: AppColor.primary,
|
|
foregroundColor: AppColor.white,
|
|
shape: const StadiumBorder(),
|
|
icon: const Icon(Icons.edit_rounded, size: 20),
|
|
label: Text(
|
|
'Tulis',
|
|
style: AppStyle.md.copyWith(
|
|
color: AppColor.white,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
SliverAppBar(
|
|
pinned: true,
|
|
automaticallyImplyLeading: false,
|
|
backgroundColor: CommunityColor.background,
|
|
title: const Text('EnakCommunity'),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: () =>
|
|
context.router.push(const NotificationRoute()),
|
|
icon: const Icon(
|
|
Icons.notifications_none_rounded,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
],
|
|
),
|
|
SliverToBoxAdapter(child: _buildHero(joinedIds.length)),
|
|
SliverPersistentHeader(
|
|
pinned: true,
|
|
delegate: _TabHeaderDelegate(
|
|
selected: _tab,
|
|
onChanged: (tab) => setState(() => _tab = tab),
|
|
),
|
|
),
|
|
...switch (_tab) {
|
|
_CommunityTab.forYou => _buildForYou(joinedIds),
|
|
_CommunityTab.groups => _buildGroups(joinedIds),
|
|
_CommunityTab.events => _buildEvents(),
|
|
},
|
|
const SliverToBoxAdapter(child: SizedBox(height: 96)),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildHero(int joinedCount) {
|
|
final myPostCount = _posts
|
|
.where((p) => p.author.id == currentMember.id)
|
|
.length;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
|
padding: const EdgeInsets.fromLTRB(20, 20, 16, 16),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [CommunityColor.blush, Color(0xFFFFF6EE)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(28),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Halo, Foodie! 👋',
|
|
style: AppStyle.xxl.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Berbagi cerita kuliner, ikut event seru, dan temukan '
|
|
'teman makan baru.',
|
|
style: AppStyle.sm.copyWith(
|
|
height: 1.45,
|
|
color: AppColor.textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Assets.icons.enakcommunityIcon.image(width: 72, height: 72),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Row(
|
|
children: [
|
|
_HeroStat(value: '$joinedCount', label: 'Komunitas'),
|
|
const SizedBox(width: 8),
|
|
_HeroStat(value: '$myPostCount', label: 'Postingan'),
|
|
const SizedBox(width: 8),
|
|
_HeroStat(value: '${_goingEventIds.length}', label: 'Event'),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Material(
|
|
color: AppColor.white,
|
|
borderRadius: BorderRadius.circular(999),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(999),
|
|
onTap: _compose,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(6, 6, 16, 6),
|
|
child: Row(
|
|
children: [
|
|
const CommunityAvatar(member: currentMember, size: 32),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'Apa yang kamu makan hari ini?',
|
|
style: AppStyle.sm.copyWith(
|
|
color: CommunityColor.muted,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
const Icon(
|
|
Icons.edit_note_rounded,
|
|
color: AppColor.primary,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildForYou(Set<String> joinedIds) {
|
|
return [
|
|
SliverToBoxAdapter(
|
|
child: CommunitySectionTitle(
|
|
title: 'Komunitas Populer',
|
|
actionLabel: 'Lihat semua',
|
|
onAction: () => setState(() => _tab = _CommunityTab.groups),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 176,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
itemCount: communityGroups.length,
|
|
separatorBuilder: (_, _) => const SizedBox(width: 12),
|
|
itemBuilder: (context, index) {
|
|
final group = communityGroups[index];
|
|
return CommunityGroupCard(
|
|
group: group,
|
|
isJoined: joinedIds.contains(group.id),
|
|
onTap: () => _openGroup(group),
|
|
onJoin: () => toggleJoinCommunity(group.id),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
const SliverToBoxAdapter(
|
|
child: CommunitySectionTitle(title: 'Obrolan Terbaru'),
|
|
),
|
|
SliverList.builder(
|
|
itemCount: _posts.length,
|
|
itemBuilder: (context, index) {
|
|
final post = _posts[index];
|
|
return CommunityPostCard(
|
|
key: ValueKey(post.id),
|
|
post: post,
|
|
onTap: () => _openPost(post),
|
|
onGroupTap: () => _openGroup(post.group),
|
|
);
|
|
},
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildGroups(Set<String> joinedIds) {
|
|
final joined = communityGroups
|
|
.where((g) => joinedIds.contains(g.id))
|
|
.toList();
|
|
final discover = communityGroups
|
|
.where((g) => !joinedIds.contains(g.id))
|
|
.toList();
|
|
|
|
Widget tile(CommunityGroup group) => CommunityGroupTile(
|
|
key: ValueKey(group.id),
|
|
group: group,
|
|
isJoined: joinedIds.contains(group.id),
|
|
onTap: () => _openGroup(group),
|
|
onJoin: () => toggleJoinCommunity(group.id),
|
|
);
|
|
|
|
return [
|
|
const SliverToBoxAdapter(
|
|
child: CommunitySectionTitle(title: 'Komunitas Saya'),
|
|
),
|
|
if (joined.isEmpty)
|
|
const SliverToBoxAdapter(
|
|
child: _SoftEmpty(
|
|
emoji: '🤝',
|
|
title: 'Belum gabung komunitas',
|
|
message: 'Pilih komunitas di bawah yang sesuai seleramu.',
|
|
),
|
|
)
|
|
else
|
|
SliverList.list(children: joined.map(tile).toList()),
|
|
if (discover.isNotEmpty) ...[
|
|
const SliverToBoxAdapter(
|
|
child: CommunitySectionTitle(title: 'Jelajahi Komunitas'),
|
|
),
|
|
SliverList.list(children: discover.map(tile).toList()),
|
|
],
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildEvents() {
|
|
return [
|
|
SliverToBoxAdapter(
|
|
child: Container(
|
|
margin: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFF8E1),
|
|
borderRadius: BorderRadius.circular(18),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Text('🎁', style: TextStyle(fontSize: 24)),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Hadiri event komunitas dan kumpulkan EnakPoint & EnakCoin '
|
|
'tambahan!',
|
|
style: AppStyle.sm.copyWith(
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SliverList.builder(
|
|
itemCount: communityEvents.length,
|
|
itemBuilder: (context, index) {
|
|
final event = communityEvents[index];
|
|
return CommunityEventCard(
|
|
event: event,
|
|
isGoing: _goingEventIds.contains(event.id),
|
|
onToggle: () => _toggleEvent(event),
|
|
);
|
|
},
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
class _HeroStat extends StatelessWidget {
|
|
const _HeroStat({required this.value, required this.label});
|
|
|
|
final String value;
|
|
final String label;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white.withOpacity(0.7),
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: '$value ',
|
|
style: const TextStyle(
|
|
color: AppColor.primary,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
TextSpan(text: label),
|
|
],
|
|
),
|
|
style: AppStyle.xs.copyWith(fontWeight: FontWeight.w600),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SoftEmpty extends StatelessWidget {
|
|
const _SoftEmpty({
|
|
required this.emoji,
|
|
required this.title,
|
|
required this.message,
|
|
});
|
|
|
|
final String emoji;
|
|
final String title;
|
|
final String message;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
|
decoration: communityCardDecoration(),
|
|
child: Column(
|
|
children: [
|
|
Text(emoji, style: const TextStyle(fontSize: 32)),
|
|
const SizedBox(height: 8),
|
|
Text(title, style: AppStyle.md.copyWith(fontWeight: FontWeight.w700)),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
message,
|
|
textAlign: TextAlign.center,
|
|
style: AppStyle.sm.copyWith(color: CommunityColor.muted),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Tab bergaya pil lembut yang menempel saat di-scroll.
|
|
class _TabHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|
_TabHeaderDelegate({required this.selected, required this.onChanged});
|
|
|
|
final _CommunityTab selected;
|
|
final ValueChanged<_CommunityTab> onChanged;
|
|
|
|
static const double _height = 64;
|
|
static const _labels = {
|
|
_CommunityTab.forYou: 'Untukmu',
|
|
_CommunityTab.groups: 'Komunitas',
|
|
_CommunityTab.events: 'Event',
|
|
};
|
|
|
|
@override
|
|
double get minExtent => _height;
|
|
|
|
@override
|
|
double get maxExtent => _height;
|
|
|
|
@override
|
|
Widget build(
|
|
BuildContext context,
|
|
double shrinkOffset,
|
|
bool overlapsContent,
|
|
) {
|
|
const tabs = _CommunityTab.values;
|
|
final index = tabs.indexOf(selected);
|
|
|
|
return Container(
|
|
color: CommunityColor.background,
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white,
|
|
borderRadius: BorderRadius.circular(999),
|
|
boxShadow: communitySoftShadow,
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
AnimatedAlign(
|
|
duration: const Duration(milliseconds: 250),
|
|
curve: Curves.easeOutCubic,
|
|
alignment: Alignment(-1 + 2 * index / (tabs.length - 1), 0),
|
|
child: FractionallySizedBox(
|
|
widthFactor: 1 / tabs.length,
|
|
heightFactor: 1,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: CommunityColor.blush,
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: tabs.map((tab) {
|
|
final isSelected = tab == selected;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => onChanged(tab),
|
|
child: Center(
|
|
child: Text(
|
|
_labels[tab]!,
|
|
style: AppStyle.sm.copyWith(
|
|
color: isSelected
|
|
? AppColor.primary
|
|
: CommunityColor.muted,
|
|
fontWeight: isSelected
|
|
? FontWeight.w700
|
|
: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRebuild(_TabHeaderDelegate oldDelegate) =>
|
|
oldDelegate.selected != selected;
|
|
}
|