Files
winded/lib/features/suggestions/presentation/widgets/suggestion_form.dart
T
philip b239ae3e5f Initial commit: Flutter app + PHP/MySQL backend on Hostinger
Replaces Firebase with a self-hosted PHP/MySQL API served from
winded.prymsolutions.com. Includes full backend (schema, auth, events,
teams, brackets, suggestions, stats, media, file upload) and updated
Flutter repositories and domain models.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:13:57 -07:00

189 lines
6.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../auth/application/auth_notifier.dart';
import '../../application/suggestions_notifier.dart';
/// Form for submitting a new community suggestion.
///
/// Owns its own [TextEditingController] and the "anonymous" toggle. Wires
/// into [suggestionsNotifierProvider] for submission state — the FilledButton
/// switches to a spinner while loading, and shows snackbars on success/error.
class SuggestionForm extends ConsumerStatefulWidget {
const SuggestionForm({super.key});
static const int _maxChars = 500;
static const int _minChars = 10;
@override
ConsumerState<SuggestionForm> createState() => _SuggestionFormState();
}
class _SuggestionFormState extends ConsumerState<SuggestionForm> {
final _formKey = GlobalKey<FormState>();
final _controller = TextEditingController();
bool _isAnonymous = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
String? _validate(String? value) {
final text = value?.trim() ?? '';
if (text.length < SuggestionForm._minChars) {
return 'Please write at least ${SuggestionForm._minChars} characters.';
}
return null;
}
Future<void> _onSubmit() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
FocusScope.of(context).unfocus();
final user = ref.read(authNotifierProvider).valueOrNull;
final messenger = ScaffoldMessenger.of(context);
await ref.read(suggestionsNotifierProvider.notifier).submit(
text: _controller.text,
isAnonymous: _isAnonymous,
userId: user?.uid,
displayName: user?.displayName,
);
if (!mounted) return;
final state = ref.read(suggestionsNotifierProvider);
state.when(
data: (_) {
_controller.clear();
_formKey.currentState?.reset();
setState(() {});
messenger.showSnackBar(
const SnackBar(content: Text('Thanks for your suggestion!')),
);
},
loading: () {},
error: (err, _) {
messenger.showSnackBar(
SnackBar(
content: Text('Could not submit suggestion: $err'),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
final submissionState = ref.watch(suggestionsNotifierProvider);
final isSubmitting = submissionState.isLoading;
final authUser = ref.watch(authNotifierProvider).valueOrNull;
final submittingAs = (!_isAnonymous && authUser != null)
? (authUser.displayName?.trim().isNotEmpty ?? false
? authUser.displayName!.trim()
: authUser.email)
: null;
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'Share an idea',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
'Tell us what would make Winded better — new features, '
'tournament formats, anything.',
style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _controller,
enabled: !isSubmitting,
minLines: 3,
maxLines: 8,
maxLength: SuggestionForm._maxChars,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
inputFormatters: <TextInputFormatter>[
LengthLimitingTextInputFormatter(SuggestionForm._maxChars),
],
validator: _validate,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
hintText: 'Type your suggestion here…',
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
),
const SizedBox(height: 8),
SwitchListTile.adaptive(
value: _isAnonymous,
onChanged: isSubmitting
? null
: (value) => setState(() => _isAnonymous = value),
contentPadding: EdgeInsets.zero,
title: const Text('Submit anonymously'),
subtitle: Text(
_isAnonymous
? 'Your name will not be attached to this suggestion.'
: 'Admins will see who submitted this.',
style: theme.textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
),
if (submittingAs != null) ...<Widget>[
const SizedBox(height: 4),
Row(
children: <Widget>[
Icon(
Icons.person_outline,
size: 16,
color: colors.onSurfaceVariant,
),
const SizedBox(width: 6),
Expanded(
child: Text(
'Submitting as: $submittingAs',
style: theme.textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
),
],
),
],
const SizedBox(height: 16),
FilledButton.icon(
onPressed: isSubmitting ? null : _onSubmit,
icon: isSubmitting
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: colors.onPrimary,
),
)
: const Icon(Icons.send_outlined),
label: Text(isSubmitting ? 'Submitting…' : 'Submit Suggestion'),
),
],
),
);
}
}