feat: add profile screen and update routing; enhance login validation and logout functionality

This commit is contained in:
Nils-Johan Gynther
2026-04-21 22:30:35 +02:00
parent fa06ba0915
commit 056d5a8a1b
7 changed files with 240 additions and 1 deletions
+37
View File
@@ -0,0 +1,37 @@
# Flutter Frontend - User Guide
This README describes how to use the Flutter frontend for Recipe App from a user and operator perspective.
Related documents:
- [next_steps_flutter.md](../next_steps_flutter.md)
- [teknisk_beskrivning_flutter.md](../teknisk_beskrivning_flutter.md)
## What this app is
This is a Flutter Web frontend for Recipe App, served in Docker and exposed through Caddy.
It is intended to behave like the existing web frontend, but built in Flutter to support future Android and iOS clients.
## Current user flows
- Login with username and password.
- Recipe list view after login.
- Profile page (base version).
- Logout from recipe/profile pages.
## Where to access it
- Test environment: `https://test.gynther.se`
## Login details
- Login expects username, not email.
- Example seeded admin user in backend bootstrap: `Nadmin`.
- Password is controlled by server environment variable (`ADMIN_NADMIN_PASSWORD`).
## Known current scope
This is an active migration track. Not all pages from the existing frontend are moved yet.
Planned migration sequence is documented in [next_steps_flutter.md](../next_steps_flutter.md).
## Troubleshooting (user level)
1. If page shows old behavior after deploy: hard refresh or open in incognito.
2. If login fails: verify username/password (not email).
3. If recipes do not load: report browser console/network errors to the dev team.
## Release expectation
This frontend is available for iterative testing. Feature parity with the current production frontend is delivered step by step.
+5
View File
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/recipes/presentation/recipes_screen.dart';
import '../../features/auth/presentation/login_screen.dart';
import '../../features/profile/presentation/profile_screen.dart';
final appRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
@@ -15,6 +16,10 @@ final appRouterProvider = Provider<GoRouter>((ref) {
path: '/recipes',
builder: (context, state) => const RecipesScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
);
});
@@ -22,6 +22,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
}
Future<void> _submit() async {
if (_usernameCtrl.text.trim().isEmpty || _passwordCtrl.text.isEmpty) {
return;
}
await ref.read(authStateProvider.notifier).login(
_usernameCtrl.text.trim(),
_passwordCtrl.text,
@@ -47,12 +50,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
TextField(
controller: _usernameCtrl,
decoration: const InputDecoration(labelText: 'Anvandarnamn'),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 12),
TextField(
controller: _passwordCtrl,
decoration: const InputDecoration(labelText: 'Lösenord'),
obscureText: true,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _submit(),
),
const SizedBox(height: 24),
if (authState is AsyncLoading)
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../auth/data/auth_providers.dart';
class ProfileScreen extends ConsumerWidget {
const ProfileScreen({super.key});
Future<void> _logout(BuildContext context, WidgetRef ref) async {
await ref.read(authStateProvider.notifier).logout();
if (context.mounted) context.go('/login');
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('Profil'),
actions: [
IconButton(
tooltip: 'Recept',
icon: const Icon(Icons.restaurant_menu),
onPressed: () => context.go('/recipes'),
),
IconButton(
tooltip: 'Logga ut',
icon: const Icon(Icons.logout),
onPressed: () => _logout(context, ref),
),
],
),
body: const Center(
child: Text('Profilsida (grundversion)'),
),
);
}
}
@@ -1,15 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../auth/data/auth_providers.dart';
import '../data/recipe_providers.dart';
class RecipesScreen extends ConsumerWidget {
const RecipesScreen({super.key});
Future<void> _logout(BuildContext context, WidgetRef ref) async {
await ref.read(authStateProvider.notifier).logout();
if (context.mounted) context.go('/login');
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final recipesAsync = ref.watch(recipesProvider);
return Scaffold(
appBar: AppBar(title: const Text('Recept')),
appBar: AppBar(
title: const Text('Recept'),
actions: [
IconButton(
tooltip: 'Profil',
icon: const Icon(Icons.person),
onPressed: () => context.go('/profile'),
),
IconButton(
tooltip: 'Logga ut',
icon: const Icon(Icons.logout),
onPressed: () => _logout(context, ref),
),
],
),
body: recipesAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Fel: $e')),