Files
GroundedHelper/frontend/lib/Grounded/see/group/GroupEditorState.dart
alvocool d3431ca5de Switch to Inter, round the corners softly and make the nav switch tabs
Inter replaces General Sans (Inter Display for titles and big numbers).
Corners come back from before the squaring commit, scaled to a soft set
(16 cards, 14 buttons, 12 rows, 8 chips) and applied to the screens built
since. The nav is now a cluster of round frosted buttons that switches
Today, Routines, Goals, Groups and Progress in place inside Home: no
pushed pages, no back arrows, and Android back returns to Today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-09 07:08:46 +03:00

295 lines
8.6 KiB
Dart

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<GroupEditor>
implements ConnectGroupEditor {
ViewGroupEditor? _model;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
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<ViewGroupEditor>.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<String>(
options: const <String>["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<Widget> _createFields() {
return <Widget>[
_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<Widget> _joinFields() {
return <Widget>[
inputField(
"Invite code",
_code,
hint: "6 characters, like MRN7AM",
validator: _validateCode,
icon: CupertinoIcons.ticket,
formatters: <TextInputFormatter>[
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(
borderRadius: const BorderRadius.all(Radius.circular(12)),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[_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 <String>["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(
borderRadius: const BorderRadius.all(Radius.circular(12)),
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);
}
}