import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:stacked/stacked.dart'; import '../../about/external/data/Group.dart'; import '../../about/external/initial/GroupRequest.dart'; import '../../about/external/initial/JoinGroupRequest.dart'; import '../../about/internal/application/TextType.dart'; import '../../designs/MemberAvatar.dart'; import '../../designs/Responsive.dart'; import '../../designs/Shell.dart'; import '../../designs/buttons/Buttons.dart'; import '../../designs/input/InputFields.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../../utils/Validators.dart'; import 'ConnectGroupEditor.dart'; import 'GroupEditor.dart'; import 'ViewGroupEditor.dart'; class GroupEditorState extends State implements ConnectGroupEditor { ViewGroupEditor? _model; final GlobalKey _formKey = GlobalKey(); final TextEditingController _name = TextEditingController(); final TextEditingController _description = TextEditingController(); final TextEditingController _code = TextEditingController(); Color _accent = goalAccents[1]; bool _joining = false; @override void initState() { super.initState(); _joining = widget.joining; } @override void dispose() { _name.dispose(); _description.dispose(); _code.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewGroupEditor(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() {} // ── Handlers ────────────────────────────────────────────────────────────── void _onBack() { Navigator.pop(context, false); } void _onModeSelected(String mode) { setState(() { _joining = mode == "Join"; }); } void _onNameChanged(String value) { setState(() {}); } void _onAccentSelected(Color colour) { setState(() { _accent = colour; }); } void _onSubmit() { if (_formKey.currentState?.validate() != true) { return; } if (_joining) { _model?.join(JoinGroupRequest(inviteCode: _code.text.trim().toUpperCase())); } else { _model?.create(GroupRequest( name: _name.text.trim(), description: _description.text.trim(), colourHex: colorToHex(_accent), )); } } String? _validateCode(String? value) { final String code = (value ?? "").trim(); if (code.length != 6) { return "Invite codes are six characters."; } return null; } // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { return Sheet( eyebrow: _joining ? "Join" : "New", title: "Group", onBack: _onBack, child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ displayTitle(_joining ? "Got a code?" : "Who's keeping\nyou honest?"), const SizedBox(height: 22), segmentedSelector( options: const ["Create", "Join"], selected: _joining ? "Join" : "Create", label: _modeLabel, onSelected: _onModeSelected, activeColor: colorNight, ), const SizedBox(height: 26), if (_joining) ..._joinFields() else ..._createFields(), const SizedBox(height: 32), roundedCornerButton( _joining ? "Join group" : "Create group", _onSubmit, icon: _joining ? CupertinoIcons.arrow_right : CupertinoIcons.add, ), ], ), ), ); } String _modeLabel(String mode) => mode; List _createFields() { return [ _preview(), const SizedBox(height: 26), inputField( "Group name", _name, hint: "Morning Crew, Thesis Sprint…", validator: Validators.title, onChanged: _onNameChanged, ), const SizedBox(height: 16), inputField( "What it's for", _description, hint: "Optional: the promise you're all making", maxLines: 3, ), const SizedBox(height: 26), sectionBreak("Colour", caption: "the group's banner"), _swatches(), const SizedBox(height: 14), text( "You'll get a six-letter invite code to send to the others once it's created.", 12, TextType.Regular, color: colorGrey2, height: 1.5, ), ]; } List _joinFields() { return [ inputField( "Invite code", _code, hint: "6 characters, like MRN7AM", validator: _validateCode, icon: CupertinoIcons.ticket, formatters: [ LengthLimitingTextInputFormatter(6), FilteringTextInputFormatter.allow(RegExp(r"[A-Za-z0-9]")), ], ), const SizedBox(height: 12), text( "Ask whoever started the group: the code is on the group's page. Joining shows you their shared goals and the chat straight away.", 12, TextType.Regular, color: colorGrey2, height: 1.5, ), ]; } /// The banner as it will look, so the colour is chosen by seeing it. Widget _preview() { final String name = _name.text.trim().isEmpty ? "Your group" : _name.text.trim(); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_accent, Color.lerp(_accent, colorBlack, 0.45)!], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon(CupertinoIcons.person_3_fill, size: 16, color: colorWhite.withValues(alpha: 0.7)), const SizedBox(width: 8), text("1 MEMBER", 9, TextType.Bold, color: colorWhite.withValues(alpha: 0.6), letterSpacing: 1.2), ], ), const SizedBox(height: 14), text(name, 26, TextType.Light, color: colorWhite, maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 16), avatarStack(const ["Alex"], 34), ], ), ); } Widget _swatches() { return Wrap( spacing: 9, runSpacing: 9, children: goalAccents.map((colour) { final bool selected = colour.toARGB32() == _accent.toARGB32(); return GestureDetector( onTap: () => _onAccentSelected(colour), child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: 40, height: 40, alignment: Alignment.center, decoration: BoxDecoration( color: colour, border: Border.all( color: selected ? colorWhite : Colors.transparent, width: 3, ), boxShadow: selected ? [BoxShadow(color: colour, spreadRadius: 1.5)] : null, ), child: selected ? Icon(Icons.check_rounded, size: 18, color: colorWhite) : null, ), ); }).toList(), ); } // ── ConnectGroupEditor ──────────────────────────────────────────────────── @override void onGroupReady(Group group) { Navigator.pop(context, group); } }