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

413 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
import 'package:inou_app/main.dart';
import 'package:inou_app/core/locale_provider.dart';
/// Navigation item for header
class NavItem {
final String label;
final String route;
final bool isExternal;
const NavItem({
required this.label,
required this.route,
this.isExternal = false,
});
}
/// inou Header - responsive, matches web design with language switcher
class InouHeader extends StatelessWidget {
final VoidCallback? onLogoTap;
final List<NavItem> navItems;
final String? currentRoute;
final VoidCallback? onLoginTap;
final VoidCallback? onSignupTap;
final bool isLoggedIn;
final String? userName;
final VoidCallback? onProfileTap;
final VoidCallback? onLogoutTap;
const InouHeader({
super.key,
this.onLogoTap,
this.navItems = const [],
this.currentRoute,
this.onLoginTap,
this.onSignupTap,
this.isLoggedIn = false,
this.userName,
this.onProfileTap,
this.onLogoutTap,
});
static const defaultNavItems = [
NavItem(label: 'Dossiers', route: '/dossiers'),
NavItem(label: 'Privacy', route: '/privacy'),
NavItem(label: 'Connect', route: '/connect'),
NavItem(label: 'Invite a friend', route: '/invite'),
NavItem(label: 'Demo', route: '/demo'),
];
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isMobile = screenWidth < 768;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: InouTheme.bg,
border: Border(
bottom: BorderSide(color: InouTheme.border, width: 1),
),
),
child: SafeArea(
bottom: false,
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 24,
vertical: 12,
),
child: isMobile ? _buildMobileHeader(context) : _buildDesktopHeader(context),
),
),
),
);
}
Widget _buildDesktopHeader(BuildContext context) {
return Row(
children: [
// Logo
_buildLogo(context),
const SizedBox(width: 48),
// Navigation
Expanded(
child: Row(
children: [
for (final item in navItems.isEmpty ? defaultNavItems : navItems)
_buildNavItem(context, item),
],
),
),
// Language switcher
_LanguageSwitcher(),
const SizedBox(width: 16),
// Auth buttons
_buildAuthSection(context),
],
);
}
Widget _buildMobileHeader(BuildContext context) {
return Row(
children: [
_buildLogo(context),
const Spacer(),
_LanguageSwitcher(),
const SizedBox(width: 8),
_buildMobileMenuButton(context),
],
);
}
Widget _buildLogo(BuildContext context) {
final l10n = AppLocalizations.of(context);
return GestureDetector(
onTap: onLogoTap,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'inou',
style: InouText.logo.copyWith(
color: InouTheme.accent,
),
),
Text(
'health',
style: InouText.logoLight.copyWith(
color: InouTheme.textMuted,
),
),
const SizedBox(width: 12),
Text(
l10n?.appTagline ?? 'ai answers for you',
style: InouText.logoTagline,
),
],
),
);
}
Widget _buildNavItem(BuildContext context, NavItem item) {
final isActive = currentRoute == item.route;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: InkWell(
onTap: () => _navigateTo(context, item),
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text(
item.label,
style: isActive ? InouText.navActive : InouText.nav,
),
),
),
);
}
Widget _buildAuthSection(BuildContext context) {
final l10n = AppLocalizations.of(context);
if (isLoggedIn) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: onProfileTap,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
CircleAvatar(
radius: 14,
backgroundColor: InouTheme.accentLight,
child: Text(
(userName ?? 'U')[0].toUpperCase(),
style: InouText.bodySmall.copyWith(
color: InouTheme.accent,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
Text(
userName ?? 'Account',
style: InouText.body,
),
],
),
),
),
],
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: onLoginTap,
child: Text(
l10n?.signIn ?? 'Log in',
style: InouText.nav,
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: onSignupTap,
style: ElevatedButton.styleFrom(
backgroundColor: InouTheme.accent,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusMd,
),
),
child: Text(l10n?.getStarted ?? 'Get started', style: InouText.button.copyWith(color: Colors.white)),
),
],
);
}
Widget _buildMobileMenuButton(BuildContext context) {
return IconButton(
icon: const Icon(Icons.menu, color: InouTheme.text),
onPressed: () => _showMobileMenu(context),
);
}
void _showMobileMenu(BuildContext context) {
final l10n = AppLocalizations.of(context);
showModalBottomSheet(
context: context,
backgroundColor: InouTheme.bgCard,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final item in navItems.isEmpty ? defaultNavItems : navItems)
ListTile(
title: Text(item.label, style: InouText.body),
trailing: item.isExternal
? Icon(Icons.open_in_new, size: 18, color: InouTheme.textMuted)
: null,
onTap: () {
Navigator.pop(context);
_navigateTo(context, item);
},
),
const Divider(height: 32),
if (!isLoggedIn) ...[
OutlinedButton(
onPressed: () {
Navigator.pop(context);
onLoginTap?.call();
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(l10n?.signIn ?? 'Log in', style: InouText.button),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
onSignupTap?.call();
},
style: ElevatedButton.styleFrom(
backgroundColor: InouTheme.accent,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(l10n?.getStarted ?? 'Get started', style: InouText.button.copyWith(color: Colors.white)),
),
] else ...[
ListTile(
leading: CircleAvatar(
backgroundColor: InouTheme.accentLight,
child: Text(
(userName ?? 'U')[0].toUpperCase(),
style: InouText.bodySmall.copyWith(color: InouTheme.accent),
),
),
title: Text(userName ?? 'Account', style: InouText.body),
onTap: () {
Navigator.pop(context);
onProfileTap?.call();
},
),
ListTile(
leading: const Icon(Icons.logout),
title: Text('Log out', style: InouText.body),
onTap: () {
Navigator.pop(context);
onLogoutTap?.call();
},
),
],
],
),
),
),
);
}
void _navigateTo(BuildContext context, NavItem item) {
if (item.isExternal) {
// Handle external links (url_launcher would be needed)
return;
}
Navigator.pushNamed(context, item.route);
}
}
/// Language switcher dropdown matching Go version .lang-menu
class _LanguageSwitcher extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Locale>(
valueListenable: localeNotifier,
builder: (context, locale, _) {
final currentCode = LocaleProvider.localeCodes[locale.languageCode] ?? 'EN';
return PopupMenuButton<Locale>(
offset: const Offset(0, 40),
tooltip: 'Change language',
shape: RoundedRectangleBorder(
borderRadius: InouTheme.borderRadiusMd,
),
color: InouTheme.bgCard,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
border: Border.all(color: InouTheme.border),
borderRadius: InouTheme.borderRadiusSm,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
currentCode,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 4),
Icon(
Icons.keyboard_arrow_down,
size: 16,
color: InouTheme.textMuted,
),
],
),
),
onSelected: (selectedLocale) {
InouApp.setLocale(context, selectedLocale);
},
itemBuilder: (context) => [
for (final supportedLocale in LocaleProvider.supportedLocales)
PopupMenuItem<Locale>(
value: supportedLocale,
child: Row(
children: [
Text(
LocaleProvider.localeNames[supportedLocale.languageCode] ?? '',
style: InouText.bodySmall.copyWith(
color: locale.languageCode == supportedLocale.languageCode
? InouTheme.accent
: InouTheme.text,
fontWeight: locale.languageCode == supportedLocale.languageCode
? FontWeight.w600
: FontWeight.w400,
),
),
if (locale.languageCode == supportedLocale.languageCode) ...[
const SizedBox(width: 8),
Icon(Icons.check, size: 16, color: InouTheme.accent),
],
],
),
),
],
);
},
);
}
}