Initial Flutter scaffold: Riverpod + GoRouter + Dio

This commit is contained in:
2026-01-27 13:20:01 +00:00
commit c8ac4ec8bc
18 changed files with 2728 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../shared/services/api_client.dart';
final emailsProvider = FutureProvider.autoDispose<List<Map<String, dynamic>>>((ref) async {
final apiClient = ref.watch(apiClientProvider);
return apiClient.getEmails();
});
class EmailsScreen extends ConsumerWidget {
const EmailsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final emailsAsync = ref.watch(emailsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Emails'),
),
body: emailsAsync.when(
data: (emails) {
if (emails.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.email_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'No emails yet',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
'Generate an email from a client profile',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
);
}
final drafts = emails.where((e) => e['status'] == 'draft').toList();
final sent = emails.where((e) => e['status'] == 'sent').toList();
return DefaultTabController(
length: 2,
child: Column(
children: [
const TabBar(
tabs: [
Tab(text: 'Drafts'),
Tab(text: 'Sent'),
],
),
Expanded(
child: TabBarView(
children: [
_EmailList(emails: drafts, isDraft: true, ref: ref),
_EmailList(emails: sent, isDraft: false, ref: ref),
],
),
),
],
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, s) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red.shade300),
const SizedBox(height: 16),
const Text('Failed to load emails'),
TextButton(
onPressed: () => ref.invalidate(emailsProvider),
child: const Text('Retry'),
),
],
),
),
),
);
}
}
class _EmailList extends StatelessWidget {
final List<Map<String, dynamic>> emails;
final bool isDraft;
final WidgetRef ref;
const _EmailList({
required this.emails,
required this.isDraft,
required this.ref,
});
@override
Widget build(BuildContext context) {
if (emails.isEmpty) {
return Center(
child: Text(
isDraft ? 'No drafts' : 'No sent emails',
style: TextStyle(color: Colors.grey.shade600),
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: emails.length,
itemBuilder: (context, index) {
final email = emails[index];
final dateFormat = DateFormat.yMMMd().add_jm();
final date = email['sentAt'] ?? email['createdAt'];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(
email['subject'] ?? 'No subject',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
email['content']?.toString().substring(0,
email['content'].toString().length > 100 ? 100 : email['content'].toString().length) ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey.shade600),
),
const SizedBox(height: 4),
Text(
date != null ? dateFormat.format(DateTime.parse(date)) : '',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
],
),
trailing: isDraft
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.send),
onPressed: () async {
try {
await ref.read(apiClientProvider).sendEmail(email['id']);
ref.invalidate(emailsProvider);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to send: $e')),
);
}
}
},
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Draft'),
content: const Text('Are you sure?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Delete'),
),
],
),
);
if (confirm == true) {
await ref.read(apiClientProvider).deleteEmail(email['id']);
ref.invalidate(emailsProvider);
}
},
),
],
)
: email['aiGenerated'] == true
? const Chip(
label: Text('AI'),
visualDensity: VisualDensity.compact,
)
: null,
isThreeLine: true,
),
);
},
);
}
}