From 056d5a8a1bb866a4fe3a9fae0b4c5384ae644c3b Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Tue, 21 Apr 2026 22:30:35 +0200 Subject: [PATCH] feat: add profile screen and update routing; enhance login validation and logout functionality --- flutter/README.md | 37 +++++++++ flutter/lib/core/router/app_router.dart | 5 ++ .../auth/presentation/login_screen.dart | 6 ++ .../profile/presentation/profile_screen.dart | 37 +++++++++ .../recipes/presentation/recipes_screen.dart | 23 +++++- next_steps_flutter.md | 52 ++++++++++++ teknisk_beskrivning_flutter.md | 81 +++++++++++++++++++ 7 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 flutter/README.md create mode 100644 flutter/lib/features/profile/presentation/profile_screen.dart create mode 100644 next_steps_flutter.md create mode 100644 teknisk_beskrivning_flutter.md diff --git a/flutter/README.md b/flutter/README.md new file mode 100644 index 00000000..635d7725 --- /dev/null +++ b/flutter/README.md @@ -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. diff --git a/flutter/lib/core/router/app_router.dart b/flutter/lib/core/router/app_router.dart index f9869a2b..c07d85f7 100644 --- a/flutter/lib/core/router/app_router.dart +++ b/flutter/lib/core/router/app_router.dart @@ -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((ref) { return GoRouter( @@ -15,6 +16,10 @@ final appRouterProvider = Provider((ref) { path: '/recipes', builder: (context, state) => const RecipesScreen(), ), + GoRoute( + path: '/profile', + builder: (context, state) => const ProfileScreen(), + ), ], ); }); diff --git a/flutter/lib/features/auth/presentation/login_screen.dart b/flutter/lib/features/auth/presentation/login_screen.dart index 3ef19094..0a908de9 100644 --- a/flutter/lib/features/auth/presentation/login_screen.dart +++ b/flutter/lib/features/auth/presentation/login_screen.dart @@ -22,6 +22,9 @@ class _LoginScreenState extends ConsumerState { } Future _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 { 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) diff --git a/flutter/lib/features/profile/presentation/profile_screen.dart b/flutter/lib/features/profile/presentation/profile_screen.dart new file mode 100644 index 00000000..7d27ecb5 --- /dev/null +++ b/flutter/lib/features/profile/presentation/profile_screen.dart @@ -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 _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)'), + ), + ); + } +} diff --git a/flutter/lib/features/recipes/presentation/recipes_screen.dart b/flutter/lib/features/recipes/presentation/recipes_screen.dart index b8a6f482..e10562de 100644 --- a/flutter/lib/features/recipes/presentation/recipes_screen.dart +++ b/flutter/lib/features/recipes/presentation/recipes_screen.dart @@ -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 _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')), diff --git a/next_steps_flutter.md b/next_steps_flutter.md new file mode 100644 index 00000000..371683eb --- /dev/null +++ b/next_steps_flutter.md @@ -0,0 +1,52 @@ +# Next Steps: Flutter-migrering (Alternativ 3) + +Relaterade dokument: +- [flutter/README.md](flutter/README.md) +- [teknisk_beskrivning_flutter.md](teknisk_beskrivning_flutter.md) + +## 1. Definiera malbild och scope forst +- Bestam vilka floden som maste vara parity i v1: login, receptlista, receptdetalj, inventarie, matsedel, profil. +- Satt tydlig definition of done per feature: UI, navigation, API, felhantering, loading states, auth-skydd. + +## 2. Bygg gemensam app-shell innan fler sidor +- Stabil routingstruktur. +- Gemensam navigation (top/bottom/nav drawer). +- Auth-gate och logout-flode. +- Standardkomponenter for tomma lagen, felmeddelanden och laddning. + +Det gor att varje ny sida gar snabbare och mer konsekvent. + +## 3. Migrera i denna ordning (hogst affarsvarde forst) +- Auth: login, session, logout. +- Recept: lista -> detalj -> skapa/andra. +- Inventarie: lista -> skapa -> uppdatera -> forbrukning. +- Matsedel. +- Profil/admin. + +Ordningen minimerar blockerare eftersom recept + auth ofta anvands av allt annat. + +## 4. Kor API-contract first per feature +- Verifiera exakt request/response mot backend innan UI putsas. +- Mappa datamodeller robust (null, typskillnader, fallback-falt). +- Lagg in central felhantering for 401/403/500 tidigt. + +## 5. Satt enhetliga kvalitetsgrindar per migrerad feature +- Manuell testlista for kritiska scenarier. +- En liten smoke-test efter varje deploy. +- Kontroll att web + mobilanpassning fungerar (utan web-specifika genvagar). + +## 6. Leverera i korta iterationer +- 1 feature at gangen till testmiljo. +- Demo + snabb feedback. +- Justera innan nasta feature. + +Det minskar risken att du bygger fel saker for langt. + +## 7. Avveckla gamla frontend stegvis +- Kor dubbel drift under en period. +- Peka en testdoman mot Flutter tills parity ar bekräftad. +- Flytta trafik gradvis nar karnfloden ar stabila. + +## Tumregel +- Sikta pa funktionell parity forst, pixel-perfect parity senare. +- Det ger snabbare nytta och farre regressionsproblem. diff --git a/teknisk_beskrivning_flutter.md b/teknisk_beskrivning_flutter.md new file mode 100644 index 00000000..971a7b26 --- /dev/null +++ b/teknisk_beskrivning_flutter.md @@ -0,0 +1,81 @@ +# Teknisk Beskrivning - Flutter Frontend + +Detta dokument beskriver vad som ar byggt, arkitekturval och teknisk kontext for Flutter-frontenden. +Malgrupp: utvecklare och AI-assistent i denna chat. + +Relaterade dokument: +- [next_steps_flutter.md](next_steps_flutter.md) +- [flutter/README.md](flutter/README.md) + +## Syfte +- Isolerad Flutter-baserad frontend i separat Docker-service. +- Web forst, men med arkitektur som kan ateranvandas for Android/iOS. +- Stegvis migrering av funktioner fran befintlig Next.js-frontend. + +## Vad som ar gjort +- Ny compose override: [compose.flutter.yml](compose.flutter.yml). +- Ny Flutter-service: `recipe-flutter`. +- Service ansluten till natverk: + - `recipe-internal` (backend access) + - `proxy` (external Caddy reachability) +- Flutter-container serverar web build via intern Caddy. +- Exponering via extern Caddy med site `test.gynther.se` -> `recipe-flutter:5000`. +- API anrop gar same-origin via `/api` och proxas internt till `recipe-api:8080`. + +## Arkitektur +### Lager +- Presentation: skarmar och widgets i `flutter/lib/features/*/presentation`. +- State/Application: Riverpod providers/notifiers i `flutter/lib/features/*/data`. +- Data/API: `ApiClient` i `flutter/lib/core/api`. +- Platform abstraction: token storage interface i `flutter/lib/core/platform`. + +### Routing +- GoRouter i [flutter/lib/core/router/app_router.dart](flutter/lib/core/router/app_router.dart). +- Nuvarande routes: + - `/login` + - `/recipes` + - `/profile` + +### Auth +- Login endpoint: `POST /api/auth/login`. +- Backend kontrakt anvander `username` + `password`. +- Token-falt i svar: `accessToken`. +- Token lagras via `ITokenStorage` (web implementation med SharedPreferences). + +### Recipes +- Recipes endpoint: `GET /api/recipes`. +- Flutter model ar hardad for backend-falt (`name` fallback till `title`) och null-safe parsing. + +## Drift och deploy +### Build/run +- Build argument i compose: `API_BASE_URL=/api`. +- Build command: + - `docker compose -f compose.yml -f compose.flutter.yml build recipe-flutter` +- Run command: + - `docker compose -f compose.yml -f compose.flutter.yml up -d --no-deps recipe-flutter` + +### Viktiga verifieringar +- Compose merge valid: + - `docker compose -f compose.yml -f compose.flutter.yml config` +- Container reachable from external Caddy: + - `docker exec caddy wget -S -O- http://recipe-flutter:5000` +- Public endpoint: + - `curl -I https://test.gynther.se` + +## Viktiga beslut +- `compose.yml` lamnas orord; Flutter ligger i separat override-fil. +- Flutter-web anvander same-origin API (`/api`) for att undvika mixed-content. +- Intern Caddy i Flutter-container hanterar static hosting + `/api` proxy. +- Feature migration sker stegvis enligt [next_steps_flutter.md](next_steps_flutter.md). + +## Kanda fallgropar +- Om `recipe-flutter` inte ar i `proxy` natverket blir det 502 fran extern Caddy. +- Om browser visar gammal JS kan gamla API-URL:er leva kvar i cache/service worker. +- Login med email fungerar inte om backend forvantar username. + +## Nasta tekniska steg +Fortsatt migrering enligt prioritering i [next_steps_flutter.md](next_steps_flutter.md): +1. Auth parity (session behavior refinements). +2. Recipes parity (list -> detail -> create/edit). +3. Inventory and meal plan pages. +4. Profile/admin parity.