inou/app/lib/design/widgets/inou_page.dart

330 lines
9.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
import 'package:inou_app/design/widgets/inou_header.dart';
import 'package:inou_app/design/widgets/inou_footer.dart';
/// Page scaffold with header and footer
///
/// Use [InouPage] for public pages (landing, security, FAQ, etc.)
/// Use [InouAuthPage] for authenticated pages (dashboard, dossier)
class InouPage extends StatelessWidget {
final Widget child;
final String? currentRoute;
final bool showHeader;
final bool showFooter;
final List<NavItem>? navItems;
final bool isLoggedIn;
final String? userName;
final VoidCallback? onLoginTap;
final VoidCallback? onSignupTap;
final VoidCallback? onProfileTap;
final VoidCallback? onLogoutTap;
final EdgeInsets? padding;
final bool centerContent;
final double? maxWidth;
const InouPage({
super.key,
required this.child,
this.currentRoute,
this.showHeader = true,
this.showFooter = true,
this.navItems,
this.isLoggedIn = false,
this.userName,
this.onLoginTap,
this.onSignupTap,
this.onProfileTap,
this.onLogoutTap,
this.padding,
this.centerContent = true,
this.maxWidth,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: InouTheme.bg,
body: Column(
children: [
if (showHeader)
InouHeader(
currentRoute: currentRoute,
navItems: navItems ?? InouHeader.defaultNavItems,
isLoggedIn: isLoggedIn,
userName: userName,
onLogoTap: () => Navigator.pushNamedAndRemoveUntil(
context,
'/',
(route) => false,
),
onLoginTap: onLoginTap ?? () => Navigator.pushNamed(context, '/login'),
onSignupTap: onSignupTap ?? () => Navigator.pushNamed(context, '/signup'),
onProfileTap: onProfileTap,
onLogoutTap: onLogoutTap,
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
_buildContent(context),
if (showFooter) const InouFooter(),
],
),
),
),
],
),
);
}
Widget _buildContent(BuildContext context) {
final content = centerContent
? Center(
child: Container(
constraints: BoxConstraints(
maxWidth: maxWidth ?? InouTheme.maxWidth,
),
padding: padding ?? const EdgeInsets.all(24),
child: child,
),
)
: Padding(
padding: padding ?? const EdgeInsets.all(24),
child: child,
);
return content;
}
}
/// Authenticated page scaffold with mandatory header
///
/// For deep pages like dashboard and dossier
class InouAuthPage extends StatelessWidget {
final Widget child;
final String? currentRoute;
final String? title;
final List<Widget>? actions;
final String userName;
final VoidCallback onProfileTap;
final VoidCallback onLogoutTap;
final bool showBackButton;
final VoidCallback? onBackTap;
final EdgeInsets? padding;
final double? maxWidth;
const InouAuthPage({
super.key,
required this.child,
this.currentRoute,
this.title,
this.actions,
required this.userName,
required this.onProfileTap,
required this.onLogoutTap,
this.showBackButton = false,
this.onBackTap,
this.padding,
this.maxWidth,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: InouTheme.bg,
body: SafeArea(
child: Column(
children: [
_buildAuthHeader(context),
Expanded(
child: SingleChildScrollView(
child: Center(
child: Container(
constraints: BoxConstraints(
maxWidth: maxWidth ?? InouTheme.maxWidth,
),
padding: padding ?? const EdgeInsets.all(24),
child: child,
),
),
),
),
],
),
),
);
}
Widget _buildAuthHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: InouTheme.bg,
border: Border(
bottom: BorderSide(color: InouTheme.border),
),
),
child: Row(
children: [
if (showBackButton)
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: onBackTap ?? () => Navigator.pop(context),
color: InouTheme.text,
),
if (title != null) ...[
if (showBackButton) const SizedBox(width: 8),
Text(title!, style: InouText.h3),
] else ...[
// Logo
GestureDetector(
onTap: () => Navigator.pushNamedAndRemoveUntil(
context,
'/dashboard',
(route) => false,
),
child: Text(
'inou',
style: InouText.h3.copyWith(
fontWeight: FontWeight.w700,
color: InouTheme.accent,
),
),
),
],
const Spacer(),
if (actions != null)
Row(
mainAxisSize: MainAxisSize.min,
children: actions!,
),
const SizedBox(width: 12),
// User menu
PopupMenuButton<String>(
offset: const Offset(0, 48),
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusLg,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 16,
backgroundColor: InouTheme.accentLight,
child: Text(
userName[0].toUpperCase(),
style: InouText.bodySmall.copyWith(
color: InouTheme.accent,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
Icon(Icons.keyboard_arrow_down, color: InouTheme.textMuted, size: 20),
],
),
onSelected: (value) {
switch (value) {
case 'profile':
onProfileTap();
break;
case 'logout':
onLogoutTap();
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'profile',
child: Row(
children: [
Icon(Icons.person_outline, size: 20, color: InouTheme.text),
const SizedBox(width: 12),
Text('Profile', style: InouText.body),
],
),
),
const PopupMenuDivider(),
PopupMenuItem(
value: 'logout',
child: Row(
children: [
Icon(Icons.logout, size: 20, color: InouTheme.danger),
const SizedBox(width: 12),
Text('Log out', style: InouText.body.copyWith(color: InouTheme.danger)),
],
),
),
],
),
],
),
);
}
}
/// Minimal page for auth flows (login, signup, forgot password)
class InouAuthFlowPage extends StatelessWidget {
final Widget child;
final bool showLogo;
final double? maxWidth;
const InouAuthFlowPage({
super.key,
required this.child,
this.showLogo = true,
this.maxWidth,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: InouTheme.bg,
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Container(
constraints: BoxConstraints(
maxWidth: maxWidth ?? InouTheme.maxWidthForm,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (showLogo) ...[
Text(
'inou',
style: InouTheme.pageTitle.copyWith(
color: InouTheme.accent,
letterSpacing: -1,
),
),
const SizedBox(height: 8),
Text(
'Your health, understood.',
style: InouText.body.copyWith(
color: InouTheme.textMuted,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 48),
],
child,
],
),
),
),
),
),
);
}
}