413 lines
13 KiB
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),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|