feat(ui): revamp onboarding, map, capture/review, and theme
- Redesign WelcomeScreen and OnboardingFlow with gradients, branding, and updated messaging; refresh bottom navigation styling and animations - Enhance MapScreen with severity-based marker styling, category icons, improved cluster bubbles, legend overlay, and polished filter modal; refine camera fit and device centering logic - Revamp CaptureScreen and ReviewScreen UX with modern cards, AI analysis section, improved selection controls, and clearer notes/location presentation - Update theme to civic palette with expanded neutrals, surface containers, and refined button styles - Expand design tokens: add civic colors, light variants, and gradient definitions; align semantic mappings - Refresh English copy in en.json for welcome and onboarding - Redesign ReportCard with icons, severity/status chips, improved layout, and action menus Note: some new UI strings are temporarily hardcoded (English) and will be moved to i18n assets in a follow-up.
This commit is contained in:
627
lib/app.dart
627
lib/app.dart
@@ -28,12 +28,11 @@ class FixMateApp extends StatelessWidget {
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('ms'),
|
||||
],
|
||||
supportedLocales: const [Locale('en'), Locale('ms')],
|
||||
localeResolutionCallback: (locale, supported) {
|
||||
debugPrint('[i18n] localeResolution: device=$locale, supported=$supported');
|
||||
debugPrint(
|
||||
'[i18n] localeResolution: device=$locale, supported=$supported',
|
||||
);
|
||||
if (locale == null) return supported.first;
|
||||
for (final s in supported) {
|
||||
if (s.languageCode == locale.languageCode) {
|
||||
@@ -43,7 +42,9 @@ class FixMateApp extends StatelessWidget {
|
||||
return supported.first;
|
||||
},
|
||||
builder: (context, child) {
|
||||
debugPrint('[i18n] Building MaterialApp; locale=${localeProvider.locale}');
|
||||
debugPrint(
|
||||
'[i18n] Building MaterialApp; locale=${localeProvider.locale}',
|
||||
);
|
||||
return child!;
|
||||
},
|
||||
home: const StartRouter(),
|
||||
@@ -83,7 +84,10 @@ class _MainScreenState extends State<MainScreen> {
|
||||
body: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
final offsetAnimation = Tween<Offset>(begin: const Offset(0.0, 0.02), end: Offset.zero).animate(animation);
|
||||
final offsetAnimation = Tween<Offset>(
|
||||
begin: const Offset(0.0, 0.02),
|
||||
end: Offset.zero,
|
||||
).animate(animation);
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(position: offsetAnimation, child: child),
|
||||
@@ -94,32 +98,98 @@ class _MainScreenState extends State<MainScreen> {
|
||||
child: _screens[_selectedIndex],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _selectedIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
},
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
label: I18n.t(_navLabels[0]),
|
||||
bottomNavigationBar: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.map),
|
||||
label: I18n.t(_navLabels[1]),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).shadowColor.withOpacity(0.15),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
currentIndex: _selectedIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
},
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
||||
unselectedItemColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.6),
|
||||
selectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.list),
|
||||
label: I18n.t(_navLabels[2]),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
label: I18n.t(_navLabels[3]),
|
||||
),
|
||||
],
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedIndex == 0
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.camera_alt, size: 24),
|
||||
),
|
||||
label: I18n.t(_navLabels[0]),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedIndex == 1
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.map, size: 24),
|
||||
),
|
||||
label: I18n.t(_navLabels[1]),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedIndex == 2
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.list, size: 24),
|
||||
),
|
||||
label: I18n.t(_navLabels[2]),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedIndex == 3
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.settings, size: 24),
|
||||
),
|
||||
label: I18n.t(_navLabels[3]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -133,12 +203,8 @@ class PlaceholderScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
body: Center(
|
||||
child: Text('$title - Coming Soon!'),
|
||||
),
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: Center(child: Text('$title - Coming Soon!')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -157,15 +223,15 @@ class _StartRouterState extends State<StartRouter> {
|
||||
|
||||
static const String _kOnboardedKey = 'onboarded_v1';
|
||||
static const String _kUserModeKey = 'user_mode';
|
||||
|
||||
|
||||
String? _userMode;
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
|
||||
Future<void> _load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final flag = prefs.getBool(_kOnboardedKey) ?? false;
|
||||
@@ -178,7 +244,7 @@ class _StartRouterState extends State<StartRouter> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _setGuestMode() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kUserModeKey, 'guest');
|
||||
@@ -188,7 +254,7 @@ class _StartRouterState extends State<StartRouter> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _setOnboarded({bool asGuest = false}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_kOnboardedKey, true);
|
||||
@@ -205,11 +271,13 @@ class _StartRouterState extends State<StartRouter> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('[i18n] StartRouter: hasMaterial=${Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null} locale=${Localizations.localeOf(context)}');
|
||||
|
||||
debugPrint(
|
||||
'[i18n] StartRouter: hasMaterial=${Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null} locale=${Localizations.localeOf(context)}',
|
||||
);
|
||||
|
||||
Widget screen;
|
||||
String screenKey;
|
||||
|
||||
|
||||
if (_loading) {
|
||||
screen = const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
screenKey = 'loading';
|
||||
@@ -246,22 +314,22 @@ class _StartRouterState extends State<StartRouter> {
|
||||
screen = const MainScreen();
|
||||
screenKey = 'main';
|
||||
}
|
||||
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 420),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(begin: const Offset(0.0, 0.02), end: Offset.zero).animate(animation),
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, 0.02),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(screenKey),
|
||||
child: screen,
|
||||
),
|
||||
child: KeyedSubtree(key: ValueKey(screenKey), child: screen),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -283,58 +351,173 @@ class WelcomeScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: cs.primary,
|
||||
child: const Icon(Icons.build, color: Colors.white),
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFF8FAFC), Color(0xFFF1F5F9)],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
// Enhanced header with better branding
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF2563EB), Color(0xFF3B82F6)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF2563EB).withOpacity(0.25),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.build,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'FixMate',
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Civic Solutions',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: cs.onSurface.withOpacity(0.7)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
// Enhanced main content with better visuals
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Hero icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF22C55E), Color(0xFF4ADE80)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.camera_alt,
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Spot it. Snap it. Fix it.',
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Report city issues in seconds with AI-powered detection. Help create safer, better communities together.',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: cs.onSurface.withOpacity(0.8),
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(I18n.t('app.name'), style: Theme.of(context).textTheme.titleLarge),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
I18n.t('welcome.title'),
|
||||
style: Theme.of(context).textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
I18n.t('welcome.subtitle'),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: cs.onSurface.withOpacity(0.75)),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onContinue,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: Text(I18n.t('cta.continueGuest')),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onSignIn,
|
||||
icon: const Icon(Icons.login),
|
||||
label: Text(I18n.t('cta.signIn')),
|
||||
const Spacer(),
|
||||
// Enhanced buttons
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onContinue,
|
||||
icon: const Icon(Icons.arrow_forward, size: 20),
|
||||
label: Text(
|
||||
'Continue as Guest',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.onPrimary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: onSkip,
|
||||
child: Text(I18n.t('cta.skip')),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onSignIn,
|
||||
icon: const Icon(Icons.login, size: 20),
|
||||
label: Text(
|
||||
'Sign In',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: cs.primary, width: 2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
foregroundColor: cs.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: onSkip,
|
||||
child: Text(
|
||||
'Skip for now',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -342,7 +525,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Three-step onboarding flow with concise benefits
|
||||
/// Enhanced onboarding flow with engaging civic messaging
|
||||
class OnboardingFlow extends StatefulWidget {
|
||||
const OnboardingFlow({super.key});
|
||||
|
||||
@@ -356,7 +539,10 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
|
||||
void _next() {
|
||||
if (_index < 2) {
|
||||
_pc.nextPage(duration: const Duration(milliseconds: 220), curve: Curves.easeOutCubic);
|
||||
_pc.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
@@ -365,53 +551,209 @@ class _OnboardingFlowState extends State<OnboardingFlow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
Widget page(String titleKey, String bodyKey, IconData icon) {
|
||||
|
||||
Widget _buildPage({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required String description,
|
||||
required IconData icon,
|
||||
required Color gradientStart,
|
||||
required Color gradientEnd,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircleAvatar(radius: 48, backgroundColor: cs.primary, child: Icon(icon, color: Colors.white, size: 40)),
|
||||
const SizedBox(height: 24),
|
||||
Text(I18n.t(titleKey), style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 12),
|
||||
Text(I18n.t(bodyKey), style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center),
|
||||
// Enhanced icon container with gradient
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: [gradientStart, gradientEnd]),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: gradientStart.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 64),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: cs.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: cs.onSurface.withOpacity(0.8),
|
||||
height: 1.6,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(I18n.t('onboarding.header')),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pc,
|
||||
onPageChanged: (i) => setState(() => _index = i),
|
||||
children: [
|
||||
page('onboarding.title1', 'onboarding.body1', Icons.flash_on),
|
||||
page('onboarding.title2', 'onboarding.body2', Icons.map),
|
||||
page('onboarding.title3', 'onboarding.body3', Icons.check_circle),
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFF8FAFC), Color(0xFFF1F5F9)],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Row(
|
||||
children: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(I18n.t('cta.skip'))),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: _next,
|
||||
child: Text(_index < 2 ? I18n.t('cta.next') : I18n.t('cta.getStarted')),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Enhanced header
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF2563EB), Color(0xFF3B82F6)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.build,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Welcome to FixMate',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Page view
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pc,
|
||||
onPageChanged: (i) => setState(() => _index = i),
|
||||
children: [
|
||||
_buildPage(
|
||||
title: 'Fast Issue Reporting',
|
||||
subtitle: 'AI-Powered Detection',
|
||||
description:
|
||||
'Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.',
|
||||
icon: Icons.camera_alt,
|
||||
gradientStart: const Color(0xFF22C55E),
|
||||
gradientEnd: const Color(0xFF4ADE80),
|
||||
),
|
||||
_buildPage(
|
||||
title: 'Smart City Mapping',
|
||||
subtitle: 'Real-Time Visualization',
|
||||
description:
|
||||
'View all reported issues on an interactive map with intelligent clustering and filtering options.',
|
||||
icon: Icons.map,
|
||||
gradientStart: const Color(0xFF2563EB),
|
||||
gradientEnd: const Color(0xFF3B82F6),
|
||||
),
|
||||
_buildPage(
|
||||
title: 'Track Progress',
|
||||
subtitle: 'Stay Informed',
|
||||
description:
|
||||
'Follow the status of your reports from submission to resolution. Help make your community better.',
|
||||
icon: Icons.check_circle,
|
||||
gradientStart: const Color(0xFFF97316),
|
||||
gradientEnd: const Color(0xFFFB923C),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Enhanced bottom controls
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
// Page indicator
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(3, (index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: _index == index ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _index == index ? cs.primary : cs.outline,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(
|
||||
'Skip',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: _next,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_index < 2 ? 'Next' : 'Get Started',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -433,14 +775,21 @@ class SignInScreen extends StatelessWidget {
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(I18n.t('auth.comingSoon')))),
|
||||
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(I18n.t('auth.comingSoon'))),
|
||||
),
|
||||
icon: const Icon(Icons.apple),
|
||||
label: Text(I18n.t('auth.signInWithApple')),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: cs.onSurface, foregroundColor: cs.surface),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.onSurface,
|
||||
foregroundColor: cs.surface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(I18n.t('auth.comingSoon')))),
|
||||
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(I18n.t('auth.comingSoon'))),
|
||||
),
|
||||
icon: const Icon(Icons.g_mobiledata),
|
||||
label: Text(I18n.t('auth.signInWithGoogle')),
|
||||
),
|
||||
@@ -463,4 +812,4 @@ class SignInScreen extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user