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,152 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../shared/services/api_client.dart';
final eventsProvider = FutureProvider.autoDispose
.family<List<Map<String, dynamic>>, int?>((ref, upcomingDays) async {
final apiClient = ref.watch(apiClientProvider);
return apiClient.getEvents(upcomingDays: upcomingDays ?? 30);
});
class EventsScreen extends ConsumerWidget {
const EventsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final eventsAsync = ref.watch(eventsProvider(30));
return Scaffold(
appBar: AppBar(
title: const Text('Upcoming Events'),
),
body: eventsAsync.when(
data: (events) {
if (events.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.event_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'No upcoming events',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
'Add birthdays and anniversaries to clients',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(eventsProvider(30));
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: events.length,
itemBuilder: (context, index) {
final event = events[index];
return _EventCard(event: event);
},
),
);
},
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 events'),
TextButton(
onPressed: () => ref.invalidate(eventsProvider(30)),
child: const Text('Retry'),
),
],
),
),
),
);
}
}
class _EventCard extends StatelessWidget {
final Map<String, dynamic> event;
const _EventCard({required this.event});
@override
Widget build(BuildContext context) {
final eventData = event['event'] as Map<String, dynamic>;
final client = event['client'] as Map<String, dynamic>?;
final dateFormat = DateFormat.MMMd();
final date = DateTime.parse(eventData['date']);
final daysUntil = date.difference(DateTime.now()).inDays;
IconData icon;
Color iconColor;
switch (eventData['type']) {
case 'birthday':
icon = Icons.cake;
iconColor = Colors.pink;
break;
case 'anniversary':
icon = Icons.favorite;
iconColor = Colors.red;
break;
default:
icon = Icons.event;
iconColor = Colors.blue;
}
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: iconColor.withOpacity(0.1),
child: Icon(icon, color: iconColor),
),
title: Text(eventData['title']),
subtitle: client != null
? Text('${client['firstName']} ${client['lastName']}')
: null,
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
dateFormat.format(date),
style: Theme.of(context).textTheme.titleSmall,
),
Text(
daysUntil == 0
? 'Today!'
: daysUntil == 1
? 'Tomorrow'
: 'In $daysUntil days',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: daysUntil <= 3 ? Colors.orange : Colors.grey,
fontWeight: daysUntil <= 3 ? FontWeight.bold : null,
),
),
],
),
),
);
}
}