feat: add Flutter web frontend with authentication and recipe management features

This commit is contained in:
Nils-Johan Gynther
2026-04-21 21:29:47 +02:00
parent 2acf66e4c4
commit 3996456f6f
19 changed files with 460 additions and 0 deletions
@@ -0,0 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/api/api_providers.dart';
import '../../../features/auth/data/auth_providers.dart';
import 'recipe_repository.dart';
import '../domain/recipe.dart';
final recipeRepositoryProvider = Provider<RecipeRepository>((ref) {
return RecipeRepository(ref.watch(apiClientProvider));
});
final recipesProvider = FutureProvider<List<Recipe>>((ref) async {
final token = await ref.watch(authStateProvider.future);
return ref.watch(recipeRepositoryProvider).fetchRecipes(token: token);
});
@@ -0,0 +1,20 @@
import 'dart:convert';
import '../../../core/api/api_client.dart';
import '../domain/recipe.dart';
class RecipeRepository {
final ApiClient _api;
RecipeRepository(this._api);
Future<List<Recipe>> fetchRecipes({String? token}) async {
final response = await _api.get('/api/recipes', token: token);
if (response.statusCode != 200) {
throw Exception('Failed to load recipes: ${response.statusCode}');
}
final List<dynamic> data = jsonDecode(response.body) as List<dynamic>;
return data
.map((e) => Recipe.fromJson(e as Map<String, dynamic>))
.toList();
}
}
@@ -0,0 +1,23 @@
class Recipe {
final int id;
final String title;
final String? description;
final String? imageUrl;
final int? servings;
const Recipe({
required this.id,
required this.title,
this.description,
this.imageUrl,
this.servings,
});
factory Recipe.fromJson(Map<String, dynamic> json) => Recipe(
id: json['id'] as int,
title: json['title'] as String,
description: json['description'] as String?,
imageUrl: json['imageUrl'] as String?,
servings: json['servings'] as int?,
);
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/recipe_providers.dart';
class RecipesScreen extends ConsumerWidget {
const RecipesScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final recipesAsync = ref.watch(recipesProvider);
return Scaffold(
appBar: AppBar(title: const Text('Recept')),
body: recipesAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Fel: $e')),
data: (recipes) => ListView.builder(
itemCount: recipes.length,
itemBuilder: (context, index) {
final recipe = recipes[index];
return ListTile(
leading: recipe.imageUrl != null
? Image.network(recipe.imageUrl!, width: 56, fit: BoxFit.cover)
: const Icon(Icons.restaurant),
title: Text(recipe.title),
subtitle: recipe.description != null
? Text(
recipe.description!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: null,
);
},
),
),
);
}
}