Backend (Go): - Project structure with chi router, pgxpool, goose migrations - JWT auth (access/refresh tokens) with Firebase token verification - NoopTokenVerifier for local dev without Firebase credentials - PostgreSQL user repository with atomic profile updates (transactions) - Mifflin-St Jeor calorie calculation based on profile data - REST API: POST /auth/login, /auth/refresh, /auth/logout, GET/PUT /profile, GET /health - Middleware: auth, CORS (localhost wildcard), logging, recovery, request_id - Unit tests (51 passing) and integration tests (testcontainers) - Docker Compose setup with postgres healthcheck and graceful shutdown Flutter client: - Riverpod state management with GoRouter navigation - Firebase Auth (email/password + Google sign-in with web popup support) - Platform-aware API URLs (web/Android/iOS) - Dio HTTP client with JWT auth interceptor and concurrent refresh handling - Secure token storage - Screens: Login, Register, Home (tabs: Menu, Recipes, Products, Profile) - Unit tests (17 passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
94 lines
3.0 KiB
Dart
94 lines
3.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../auth/auth_provider.dart';
|
|
import '../../features/auth/login_screen.dart';
|
|
import '../../features/auth/register_screen.dart';
|
|
import '../../features/home/home_screen.dart';
|
|
import '../../features/products/products_screen.dart';
|
|
import '../../features/menu/menu_screen.dart';
|
|
import '../../features/recipes/recipes_screen.dart';
|
|
import '../../features/profile/profile_screen.dart';
|
|
|
|
final routerProvider = Provider<GoRouter>((ref) {
|
|
final authState = ref.watch(authProvider);
|
|
|
|
return GoRouter(
|
|
initialLocation: '/home',
|
|
redirect: (context, state) {
|
|
final isLoggedIn = authState.status == AuthStatus.authenticated;
|
|
final isAuthRoute = state.matchedLocation.startsWith('/auth');
|
|
|
|
if (authState.status == AuthStatus.unknown) return null;
|
|
if (!isLoggedIn && !isAuthRoute) return '/auth/login';
|
|
if (isLoggedIn && isAuthRoute) return '/home';
|
|
return null;
|
|
},
|
|
routes: [
|
|
GoRoute(
|
|
path: '/auth/login',
|
|
builder: (_, __) => const LoginScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/auth/register',
|
|
builder: (_, __) => const RegisterScreen(),
|
|
),
|
|
ShellRoute(
|
|
builder: (context, state, child) => MainShell(child: child),
|
|
routes: [
|
|
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
|
|
GoRoute(
|
|
path: '/products',
|
|
builder: (_, __) => const ProductsScreen()),
|
|
GoRoute(path: '/menu', builder: (_, __) => const MenuScreen()),
|
|
GoRoute(
|
|
path: '/recipes', builder: (_, __) => const RecipesScreen()),
|
|
GoRoute(
|
|
path: '/profile', builder: (_, __) => const ProfileScreen()),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
});
|
|
|
|
class MainShell extends StatelessWidget {
|
|
final Widget child;
|
|
|
|
const MainShell({super.key, required this.child});
|
|
|
|
static const _tabs = [
|
|
'/home',
|
|
'/products',
|
|
'/menu',
|
|
'/recipes',
|
|
'/profile',
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final location = GoRouterState.of(context).matchedLocation;
|
|
final currentIndex = _tabs.indexOf(location).clamp(0, _tabs.length - 1);
|
|
|
|
return Scaffold(
|
|
body: child,
|
|
bottomNavigationBar: BottomNavigationBar(
|
|
currentIndex: currentIndex,
|
|
onTap: (index) => context.go(_tabs[index]),
|
|
items: const [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.home), label: 'Главная'),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.kitchen), label: 'Продукты'),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.calendar_month), label: 'Меню'),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.menu_book), label: 'Рецепты'),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.person), label: 'Профиль'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|