feat: implement API client with JSON handling and error mapping; enhance routing and state management in app shell

This commit is contained in:
Nils-Johan Gynther
2026-04-22 07:29:21 +02:00
parent 82ba334f2d
commit e8de1d3625
12 changed files with 586 additions and 133 deletions
+121
View File
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/auth/data/auth_providers.dart';
class AppShell extends ConsumerWidget {
final String location;
final Widget child;
const AppShell({
super.key,
required this.location,
required this.child,
});
static const _destinations = [
_AppDestination(
path: '/recipes',
title: 'Recept',
icon: Icons.restaurant_menu,
label: 'Recept',
),
_AppDestination(
path: '/profile',
title: 'Profil',
icon: Icons.person,
label: 'Profil',
),
];
int _selectedIndex() {
final index = _destinations.indexWhere(
(destination) => location.startsWith(destination.path),
);
return index < 0 ? 0 : index;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedIndex = _selectedIndex();
final selectedDestination = _destinations[selectedIndex];
final isWide = MediaQuery.of(context).size.width >= 900;
Future<void> logout() async {
await ref.read(authStateProvider.notifier).logout();
if (context.mounted) {
context.go('/login');
}
}
void navigateTo(int index) {
final target = _destinations[index].path;
if (target != location && context.mounted) {
context.go(target);
}
}
return Scaffold(
appBar: AppBar(
title: Text(selectedDestination.title),
actions: [
IconButton(
tooltip: 'Logga ut',
icon: const Icon(Icons.logout),
onPressed: logout,
),
],
),
body: isWide
? Row(
children: [
NavigationRail(
selectedIndex: selectedIndex,
onDestinationSelected: navigateTo,
labelType: NavigationRailLabelType.all,
destinations: _destinations
.map(
(destination) => NavigationRailDestination(
icon: Icon(destination.icon),
label: Text(destination.label),
),
)
.toList(),
),
const VerticalDivider(width: 1),
Expanded(child: child),
],
)
: child,
bottomNavigationBar: isWide
? null
: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: navigateTo,
destinations: _destinations
.map(
(destination) => NavigationDestination(
icon: Icon(destination.icon),
label: destination.label,
),
)
.toList(),
),
);
}
}
class _AppDestination {
final String path;
final String title;
final IconData icon;
final String label;
const _AppDestination({
required this.path,
required this.title,
required this.icon,
required this.label,
});
}
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
class LoadingStateView extends StatelessWidget {
final String? label;
const LoadingStateView({super.key, this.label});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
if (label != null) ...[
const SizedBox(height: 12),
Text(label!),
],
],
),
);
}
}
class EmptyStateView extends StatelessWidget {
final String title;
final String? description;
const EmptyStateView({
super.key,
required this.title,
this.description,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
if (description != null) ...[
const SizedBox(height: 8),
Text(
description!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
],
],
),
),
);
}
}
class ErrorStateView extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const ErrorStateView({
super.key,
required this.message,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
if (onRetry != null) ...[
const SizedBox(height: 12),
OutlinedButton(
onPressed: onRetry,
child: const Text('Forsok igen'),
),
],
],
),
),
);
}
}