import 'package:flutter/cupertino.dart'; import 'package:stacked/stacked.dart'; import '../../about/external/data/Group.dart'; import '../../about/internal/application/TextType.dart'; import '../../configs/Navigator.dart'; import '../../designs/Component.dart'; import '../../designs/GlassSurface.dart'; import '../../designs/MemberAvatar.dart'; import '../../designs/Responsive.dart'; import '../../designs/Shell.dart'; import '../../designs/buttons/Buttons.dart'; import '../../designs/scene/SceneArt.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../../utils/CommonUtils.dart'; import 'ConnectGroups.dart'; import 'GroupDetail.dart'; import 'GroupEditor.dart'; import 'Groups.dart'; import 'ViewGroups.dart'; class GroupsState extends State implements ConnectGroups { ViewGroups? _model; List _groups = []; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewGroups(context, this), onViewModelReady: (viewModel) { _model = viewModel; _initiate(); }, builder: (context, viewModel, child) => LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Responsive( mobile: _mobileView(constraints), tablet: _mobileView(constraints), desktop: _mobileView(constraints), ); }, ), ); } void _initiate() { _model?.loadGroups(); } // ── Handlers ────────────────────────────────────────────────────────────── void _onBack() { Navigator.pop(context, true); } void _onOpenGroup(Group group) async { await GroundedNavigation() .navigateToPageWithData(GroupDetail(group: group), context); _model?.loadGroups(); } void _onNewGroup() { _openEditor(false); } void _onJoinGroup() { _openEditor(true); } /// Created or joined, you land straight in the group. void _openEditor(bool joining) async { final result = await GroundedNavigation() .navigateToPageWithData(GroupEditor(joining: joining), context); if (result is Group) { _model?.loadGroups(); _onOpenGroup(result); } } // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { return Sheet( eyebrow: "Grounded", title: "Groups", onBack: _onBack, action: chromeAction(CupertinoIcons.add, _onNewGroup), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ displayTitle("Keep each other\nhonest."), const SizedBox(height: 14), text( "Share goals with people who will notice when you slip. Everyone logs toward the same objectives, and the chat sees every check-in.", 14, TextType.Regular, color: colorGrey2, height: 1.55, ), const SizedBox(height: 28), if (_groups.isEmpty) emptyState( CupertinoIcons.person_3, "No groups yet", "Start one and send the code to a friend, or join with a code someone sent you.", ) else ..._groups.map(_groupCard), const SizedBox(height: 24), roundedCornerButton("New group", _onNewGroup, icon: CupertinoIcons.add), const SizedBox(height: 10), outlinedActionButton("Join with a code", _onJoinGroup, icon: CupertinoIcons.ticket), ], ), ); } /// The group as a painted card: who is in it, how the shared goals are /// going, and the latest line from the chat. Widget _groupCard(Group group) { final Color accent = goalColor(group.colourHex); final Color soft = colorWhite.withValues(alpha: 0.78); return GestureDetector( onTap: () => _onOpenGroup(group), child: Container( height: 250, margin: const EdgeInsets.only(bottom: 14), decoration: const BoxDecoration( boxShadow: [ BoxShadow( color: Color(0x262A3A30), blurRadius: 24, offset: Offset(0, 10), ), ], ), child: Stack( fit: StackFit.expand, children: [ SceneArt( palette: goalScene(accent), seed: sceneSeed(group.name), figure: CupertinoIcons.person_3_fill, figureScale: 0.42, figureAlignment: const Alignment(0.7, -0.4), shade: true, ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ GlassSurface( onDark: true, padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6), child: text( "${group.members.length} members · ${group.goals.length} shared goal${group.goals.length == 1 ? "" : "s"}", 11, TextType.Medium, color: colorWhite, ), ), const Spacer(), if (group.unread > 0) Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6), color: colorLime, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(CupertinoIcons.chat_bubble_fill, size: 12, color: colorNight), const SizedBox(width: 6), text("${group.unread} new", 11, TextType.Medium, color: colorNight), ], ), ), ], ), const Spacer(), text(group.name, 24, TextType.Medium, color: colorWhite, maxLines: 1, overflow: TextOverflow.ellipsis), if (group.lastMessage.isNotEmpty) ...[ const SizedBox(height: 4), text("${group.lastAuthor}: ${group.lastMessage}", 13, TextType.Regular, color: soft, maxLines: 1, overflow: TextOverflow.ellipsis), ], const SizedBox(height: 14), meter(group.progress, fill: colorLime, track: colorWhite.withValues(alpha: 0.25), height: 6), const SizedBox(height: 12), Row( children: [ avatarStack( group.members.map((member) => member.name).toList(), 32), const Spacer(), text("${(group.progress * 100).round()}%", 22, TextType.Light, color: colorWhite), const SizedBox(width: 6), text("together", 12, TextType.Regular, color: soft), ], ), ], ), ), ], ), ), ); } // ── ConnectGroups ───────────────────────────────────────────────────────── @override void onGroupsLoaded(List groups) { setState(() { _groups = groups; }); } }