import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import '../core/theme.dart'; import '../state/session_controller.dart'; import '../widgets/common.dart'; import '../widgets/krow_wordmark.dart'; /// Splash. Routes on to onboarding, sign-in or the app depending on how far /// the worker got last time. class SplashScreen extends ConsumerStatefulWidget { const SplashScreen({super.key}); @override ConsumerState createState() => _SplashScreenState(); } class _SplashScreenState extends ConsumerState { Timer? _timer; @override void initState() { super.initState(); _timer = Timer(const Duration(milliseconds: 1400), _go); } @override void dispose() { _timer?.cancel(); super.dispose(); } void _go() { if (!mounted) return; final s = ref.read(sessionProvider); // Onboarding sits between the splash and the welcome screen for anyone // who is not signed in, rather than only on a first install — the flow is // splash, onboarding, welcome, mobile number. if (!s.signedIn) { context.go('/onboarding'); } else if (!s.permissionsDone) { context.go('/permissions'); } else { context.go('/today'); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: KrowPalette.blue, body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const KrowWordmark(width: 196), const SizedBox(height: 22), Text( 'WORK · HIRE · ELEVATE', style: TextStyle( fontSize: 10.5, letterSpacing: 3, fontWeight: FontWeight.w600, color: Colors.white.withValues(alpha: 0.6), ), ), const SizedBox(height: 44), SizedBox( width: 140, child: ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( minHeight: 3, backgroundColor: Colors.white24, valueColor: const AlwaysStoppedAnimation(KrowPalette.yellow), ), ), ), ], ), ), ); } } class _Slide { const _Slide({ required this.eyebrow, required this.title, required this.body, required this.art, required this.preview, }); final String eyebrow; final String title; final String body; /// The scene behind the copy, taken from the prototype's own artwork. final String art; /// A sample of the thing the slide is describing, floated over the artwork. /// Each one is a real piece of the app rather than an illustration. final Widget preview; } const _slides = [ _Slide( eyebrow: 'Find work', title: 'Shifts that fit around your week.', body: 'Browse open hospitality shifts near you, filtered to the hours you ' 'said you can work.', art: 'assets/art/onboarding_find_work.svg', preview: _PreviewCard( accent: KrowPalette.yellow, title: r'Barista · $21/hr', subtitle: 'Zoox · tonight, 5:50 PM', ), ), _Slide( eyebrow: 'Know your pay', title: 'See the money before you say yes.', body: 'Every shift shows the hourly rate and the total. No surprises on the ' 'payslip.', art: 'assets/art/onboarding_know_your_pay.svg', preview: _PreviewRow( label: 'Regular · 5h 48m', trailing: Text( r'$121.80', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, letterSpacing: -0.5, color: KrowPalette.ink, ), ), ), ), _Slide( eyebrow: 'Get paid', title: 'Clock in on site. Paid on schedule.', body: 'Tap in when you arrive, tap out when you leave. Timecards submit ' 'themselves.', art: 'assets/art/onboarding_get_paid.svg', preview: _PreviewRow(label: 'Paid 26 Aug', trailing: StatusChip('On time')), ), ]; /// A miniature shift card, matching the one used throughout the app: identity /// rail down the side, role and venue stacked. class _PreviewCard extends StatelessWidget { const _PreviewCard({ required this.accent, required this.title, required this.subtitle, }); final Color accent; final String title; final String subtitle; @override Widget build(BuildContext context) { return Container( width: 218, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: KrowPalette.blueDeep.withValues(alpha: 0.35), blurRadius: 24, offset: const Offset(0, 10), ), ], ), clipBehavior: Clip.antiAlias, child: Row( children: [ Container(width: 6, height: 62, color: accent), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(12, 11, 12, 11), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, letterSpacing: -0.3, ), ), const SizedBox(height: 2), Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 11.5, color: KrowPalette.mute, ), ), ], ), ), ), ], ), ); } } /// A single floated row: a label and a figure or status. class _PreviewRow extends StatelessWidget { const _PreviewRow({required this.label, required this.trailing}); final String label; final Widget trailing; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: KrowPalette.blueDeep.withValues(alpha: 0.35), blurRadius: 24, offset: const Offset(0, 10), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w600), ), const SizedBox(width: 14), trailing, ], ), ); } } class OnboardingScreen extends ConsumerStatefulWidget { const OnboardingScreen({super.key}); @override ConsumerState createState() => _OnboardingScreenState(); } class _OnboardingScreenState extends ConsumerState { final _controller = PageController(); int _page = 0; @override void dispose() { _controller.dispose(); super.dispose(); } void _finish() => context.go('/welcome'); @override Widget build(BuildContext context) { final isLast = _page == _slides.length - 1; return Scaffold( backgroundColor: KrowPalette.blue, body: Column( children: [ SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.fromLTRB(22, 8, 12, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const KrowWordmark(width: 66), TextButton( onPressed: _finish, style: TextButton.styleFrom( foregroundColor: Colors.white70, ), child: const Text('Skip'), ), ], ), ), ), Expanded( child: PageView.builder( controller: _controller, itemCount: _slides.length, onPageChanged: (i) => setState(() => _page = i), itemBuilder: (context, i) { final slide = _slides[i]; // Artwork behind, with a real piece of the app floated over // the bottom-right of it — the prototype's hero layout. return Stack( fit: StackFit.expand, children: [ // Cropped rather than letterboxed, so the scene fills the // hero on any aspect ratio the way the prototype does. SvgPicture.asset( slide.art, fit: BoxFit.cover, alignment: Alignment.topCenter, ), // Veil: darkens the foot of the artwork so the floated // card and the sheet below it keep their edges. const Positioned( left: 0, right: 0, bottom: 0, height: 140, child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0x0022366F), Color(0xF022366F)], ), ), ), ), Positioned(right: 20, bottom: 24, child: slide.preview), ], ); }, ), ), Container( width: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(24, 26, 24, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( _slides[_page].eyebrow.toUpperCase(), style: const TextStyle( fontSize: 10.5, fontWeight: FontWeight.w600, letterSpacing: 1.6, color: Color(0xFF949FA5), ), ), const SizedBox(height: 10), Text( _slides[_page].title, style: const TextStyle( fontSize: 29, fontWeight: FontWeight.w700, height: 1.1, letterSpacing: -1, ), ), const SizedBox(height: 12), Text( _slides[_page].body, style: const TextStyle( fontSize: 14.5, height: 1.55, color: KrowPalette.mute, ), ), const SizedBox(height: 22), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (var i = 0; i < _slides.length; i++) AnimatedContainer( duration: const Duration(milliseconds: 220), margin: const EdgeInsets.symmetric(horizontal: 3), width: i == _page ? 24 : 6, height: 6, decoration: BoxDecoration( color: i == _page ? KrowPalette.blue : const Color(0xFFC6D6CC), borderRadius: BorderRadius.circular(3), ), ), ], ), const SizedBox(height: 18), FilledButton( onPressed: () { if (isLast) { _finish(); } else { _controller.nextPage( duration: const Duration(milliseconds: 260), curve: Curves.easeOut, ); } }, child: Text(isLast ? 'Create your account' : 'Next'), ), if (isLast) Center( child: TextButton( onPressed: _finish, child: const Text('I already have an account'), ), ), ], ), ), ), ), ], ), ); } } class WelcomeScreen extends StatelessWidget { const WelcomeScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: KrowPalette.blue, body: Column( children: [ Expanded( child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const KrowWordmark(width: 150), const SizedBox(height: 34), const Text( 'Work the shifts\nyou actually want.', textAlign: TextAlign.center, style: TextStyle( fontSize: 32, height: 1.1, letterSpacing: -1.1, fontWeight: FontWeight.w700, color: Colors.white, ), ), const SizedBox(height: 14), Text( 'Hospitality work in your city, priced up front and paid ' 'on a fixed schedule.', textAlign: TextAlign.center, style: TextStyle( fontSize: 14.5, height: 1.55, color: Colors.white.withValues(alpha: 0.64), ), ), const SizedBox(height: 34), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _WelcomeStat('4,200', 'Shifts this month'), Container( width: 1, height: 38, margin: const EdgeInsets.symmetric(horizontal: 26), color: Colors.white24, ), _WelcomeStat(r'$21+', 'Average rate'), ], ), ], ), ), ), ), Container( width: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(22, 24, 22, 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ FilledButton( onPressed: () => context.push('/phone'), child: const Text('Continue with mobile'), ), const SizedBox(height: 10), OutlinedButton( onPressed: () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Invite codes open in the next release', ), ), ), child: const Text('I have an invite code'), ), const SizedBox(height: 12), const Text( 'By continuing you accept the Worker Terms.', style: TextStyle( fontSize: 11.5, color: Color(0xFF949FA5), ), ), ], ), ), ), ), ], ), ); } } class _WelcomeStat extends StatelessWidget { const _WelcomeStat(this.value, this.caption); final String value; final String caption; @override Widget build(BuildContext context) { return Column( children: [ Text( value, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w700, letterSpacing: -0.8, color: KrowPalette.yellow, ), ), const SizedBox(height: 3), Text( caption.toUpperCase(), style: TextStyle( fontSize: 9.5, letterSpacing: 1.1, fontWeight: FontWeight.w600, color: Colors.white.withValues(alpha: 0.55), ), ), ], ); } }