Initial commit: Fixes for linting and compilation

This commit is contained in:
2025-12-20 03:13:55 +00:00
commit 5d746d694e
148 changed files with 11207 additions and 0 deletions

View File

@@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../models/user_profile.dart';
import '../../models/cycle_entry.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
import '../../theme/app_theme.dart';
class CalendarScreen extends ConsumerStatefulWidget {
const CalendarScreen({super.key});
@override
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
}
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
CalendarFormat _calendarFormat = CalendarFormat.month;
@override
Widget build(BuildContext context) {
final entries = ref.watch(cycleEntriesProvider);
final user = ref.watch(userProfileProvider);
final cycleLength = user?.averageCycleLength ?? 28;
final lastPeriodStart = user?.lastPeriodStartDate;
return SafeArea(
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Expanded(
child: Text(
'Calendar',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
),
_buildLegendButton(),
],
),
),
// Calendar
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: TableCalendar(
firstDay: DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() => _calendarFormat = format);
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},
calendarStyle: CalendarStyle(
outsideDaysVisible: false,
defaultTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
weekendTextStyle: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
todayDecoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.3),
shape: BoxShape.circle,
),
todayTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.sageGreen,
),
selectedDecoration: const BoxDecoration(
color: AppColors.sageGreen,
shape: BoxShape.circle,
),
selectedTextStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
leftChevronIcon: Icon(
Icons.chevron_left,
color: AppColors.warmGray,
),
rightChevronIcon: Icon(
Icons.chevron_right,
color: AppColors.warmGray,
),
),
daysOfWeekStyle: DaysOfWeekStyle(
weekdayStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
weekendStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
calendarBuilders: CalendarBuilders(
markerBuilder: (context, date, events) {
// Check if it's a logged period day
final isLoggedPeriod = _isLoggedPeriodDay(date, entries);
if (isLoggedPeriod) {
return Positioned(
bottom: 1,
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: AppColors.menstrualPhase,
shape: BoxShape.circle,
),
),
);
}
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
if (phase != null) {
return Positioned(
bottom: 1,
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.5),
shape: BoxShape.circle,
),
),
);
}
return null;
},
),
),
),
const SizedBox(height: 20),
// Selected Day Info
if (_selectedDay != null)
Expanded(
child: _buildDayInfo(_selectedDay!, lastPeriodStart, cycleLength, entries),
),
],
),
);
}
Widget _buildLegendButton() {
return GestureDetector(
onTap: () => _showLegend(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: AppColors.blushPink.withOpacity(0.5),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: AppColors.rose),
const SizedBox(width: 4),
Text(
'Legend',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.rose,
),
),
],
),
),
);
}
void _showLegend() {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Legend',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 20),
_buildLegendItem(AppColors.menstrualPhase, 'Period'),
_buildLegendItem(AppColors.follicularPhase, 'Follicular Phase'),
_buildLegendItem(AppColors.ovulationPhase, 'Ovulation Window'),
_buildLegendItem(AppColors.lutealPhase, 'Luteal Phase'),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildLegendItem(Color color, String label) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Text(
label,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
),
],
),
);
}
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength, List<CycleEntry> entries) {
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
final entry = _getEntryForDate(date, entries);
final isLoggedPeriod = entry?.isPeriodDay ?? false;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${_getMonthName(date.month)} ${date.day}, ${date.year}',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 12),
if (phase != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(phase.emoji),
const SizedBox(width: 6),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
const SizedBox(height: 12),
if (isLoggedPeriod)
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.menstrualPhase.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.water_drop, color: AppColors.menstrualPhase, size: 20),
const SizedBox(width: 8),
Text(
'Period Recorded',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.menstrualPhase,
),
),
],
),
),
Text(
phase?.description ?? 'No cycle data for this date',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
],
),
);
}
CyclePhase? _getPhaseForDate(DateTime date, DateTime? lastPeriodStart, int cycleLength) {
if (lastPeriodStart == null) return null;
final daysSinceLastPeriod = date.difference(lastPeriodStart).inDays;
if (daysSinceLastPeriod < 0) return null;
final dayOfCycle = (daysSinceLastPeriod % cycleLength) + 1;
if (dayOfCycle <= 5) return CyclePhase.menstrual;
if (dayOfCycle <= 13) return CyclePhase.follicular;
if (dayOfCycle <= 16) return CyclePhase.ovulation;
return CyclePhase.luteal;
}
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
String _getMonthName(int month) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
return months[month - 1];
}
bool _isLoggedPeriodDay(DateTime date, List<CycleEntry> entries) {
final entry = _getEntryForDate(date, entries);
return entry?.isPeriodDay ?? false;
}
CycleEntry? _getEntryForDate(DateTime date, List<CycleEntry> entries) {
try {
return entries.firstWhere(
(entry) => isSameDay(entry.date, date),
);
} catch (_) {
return null;
}
}
}

View File

@@ -0,0 +1,372 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/scripture.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
import '../../models/cycle_entry.dart';
import '../../theme/app_theme.dart';
class DevotionalScreen extends ConsumerWidget {
const DevotionalScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final phase = cycleInfo['phase'] as CyclePhase;
final scripture = ScriptureDatabase.getScriptureForPhase(phase.name);
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Expanded(
child: Text(
'Today\'s Devotional',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Text(phase.emoji),
const SizedBox(width: 4),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
],
),
const SizedBox(height: 8),
Text(
phase.description,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 32),
// Main Scripture Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getPhaseColor(phase).withOpacity(0.15),
AppColors.cream,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _getPhaseColor(phase).withOpacity(0.3),
),
),
child: Column(
children: [
// Quote icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.format_quote,
color: _getPhaseColor(phase),
size: 24,
),
),
const SizedBox(height: 20),
// Verse
Text(
'"${scripture.verse}"',
textAlign: TextAlign.center,
style: GoogleFonts.lora(
fontSize: 20,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
const SizedBox(height: 16),
// Reference
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.warmGray,
),
),
],
),
),
const SizedBox(height: 24),
// Reflection
if (scripture.reflection != null) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.lightbulb_outline,
color: AppColors.softGold,
size: 20,
),
const SizedBox(width: 8),
Text(
'Reflection',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
scripture.reflection!,
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 16),
],
// Phase-specific encouragement
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.favorite_outline,
color: AppColors.rose,
size: 20,
),
const SizedBox(width: 8),
Text(
'For Your ${phase.label} Phase',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
_getPhaseEncouragement(phase, user?.isMarried ?? false),
style: GoogleFonts.outfit(
fontSize: 15,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 16),
// Prayer Prompt
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.lavender.withOpacity(0.2),
AppColors.blushPink.withOpacity(0.2),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('🙏', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Text(
'Prayer Prompt',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
const SizedBox(height: 12),
Text(
_getPrayerPrompt(phase),
style: GoogleFonts.lora(
fontSize: 14,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
],
),
),
const SizedBox(height: 24),
// Action buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.share_outlined),
label: const Text('Share'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.edit_note),
label: const Text('Journal'),
),
),
],
),
const SizedBox(height: 40),
],
),
),
);
}
// Placeholder _getCurrentPhase removed as it's now in CycleService
Color _getPhaseColor(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return AppColors.menstrualPhase;
case CyclePhase.follicular:
return AppColors.follicularPhase;
case CyclePhase.ovulation:
return AppColors.ovulationPhase;
case CyclePhase.luteal:
return AppColors.lutealPhase;
}
}
String _getPhaseEncouragement(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
return 'Your body is renewing itself. This is a sacred time for rest and reflection. '
'Don\'t push yourself too hard—God designed this phase for slowing down. '
'Use this time to draw near to Him in quietness.';
case CyclePhase.follicular:
return 'Energy is returning! Your body is preparing for the days ahead. '
'This is a wonderful time to tackle projects, connect with friends, and serve others. '
'Let your renewed strength be used for His purposes.';
case CyclePhase.ovulation:
if (isMarried) {
return 'You are in your most fertile window. Whether you\'re hoping to conceive or practicing NFP, '
'remember that God is sovereign over the womb. Trust His timing and purposes for your family.';
}
return 'You may feel more confident and social during this phase. '
'It\'s a great time for important conversations, presentations, or stepping out in faith. '
'Let your light shine before others.';
case CyclePhase.luteal:
return 'The luteal phase can bring challenging emotions and PMS symptoms. '
'Be patient with yourself. This is not weakness—it\'s your body doing what God designed. '
'Lean into His peace that surpasses understanding.';
}
}
String _getPrayerPrompt(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return '"Lord, thank You for designing my body with such wisdom. '
'Help me to rest in You during this time and to trust that You are renewing me. '
'May I find my strength in Your presence. Amen."';
case CyclePhase.follicular:
return '"Father, thank You for this season of renewed energy. '
'Guide me to use this strength for Your glory and the good of others. '
'Help me to serve with joy and purpose. Amen."';
case CyclePhase.ovulation:
return '"Creator God, I am fearfully and wonderfully made. '
'Thank You for the gift of womanhood. '
'Help me to honor You in all I do today. Amen."';
case CyclePhase.luteal:
return '"Lord, I bring my anxious thoughts to You. '
'When my emotions feel overwhelming, remind me of Your peace. '
'Help me to be gentle with myself as You are gentle with me. Amen."';
}
}
}

View File

@@ -0,0 +1,406 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../../theme/app_theme.dart';
import '../../models/user_profile.dart';
import '../../models/cycle_entry.dart';
import '../../models/scripture.dart';
import '../calendar/calendar_screen.dart';
import '../log/log_screen.dart';
import '../devotional/devotional_screen.dart';
import '../../widgets/tip_card.dart';
import '../../widgets/cycle_ring.dart';
import '../../widgets/scripture_card.dart';
import '../../widgets/quick_log_buttons.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: [
const _DashboardTab(),
const CalendarScreen(),
const LogScreen(),
const DevotionalScreen(),
_SettingsTab(onReset: () => setState(() => _selectedIndex = 0)),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_today_outlined),
activeIcon: Icon(Icons.calendar_today),
label: 'Calendar',
),
BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline),
activeIcon: Icon(Icons.add_circle),
label: 'Log',
),
BottomNavigationBarItem(
icon: Icon(Icons.menu_book_outlined),
activeIcon: Icon(Icons.menu_book),
label: 'Devotional',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings',
),
],
),
),
);
}
}
class _DashboardTab extends ConsumerWidget {
const _DashboardTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final name = user?.name ?? 'Friend';
final phase = cycleInfo['phase'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
final cycleLength = user?.averageCycleLength ?? 28;
// Get scripture for current phase
final scripture = ScriptureDatabase.getScriptureForPhase(phase.name);
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Greeting
_buildGreeting(name),
const SizedBox(height: 24),
// Cycle Ring
Center(
child: CycleRing(
dayOfCycle: dayOfCycle,
totalDays: cycleLength,
phase: phase,
),
),
const SizedBox(height: 24),
// Scripture Card
ScriptureCard(
verse: scripture.verse,
reference: scripture.reference,
phase: phase,
),
const SizedBox(height: 20),
// Quick Log Buttons
Text(
'Quick Log',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 12),
const QuickLogButtons(),
const SizedBox(height: 20),
// Today's Tip - Only show if not just tracking or husband (though husband has own screen)
if (user?.role == UserRole.wife)
TipCard(phase: phase, isMarried: user?.isMarried ?? false),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildGreeting(String name) {
final hour = DateTime.now().hour;
String greeting;
if (hour < 12) {
greeting = 'Good morning';
} else if (hour < 17) {
greeting = 'Good afternoon';
} else {
greeting = 'Good evening';
}
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$greeting,',
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.warmGray,
),
),
Text(
name,
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
],
),
),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.blushPink,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.notifications_outlined,
color: AppColors.rose,
),
),
],
);
}
// Placeholder _calculateCycleInfo removed as it's now in CycleService
}
class _SettingsTab extends ConsumerWidget {
final VoidCallback? onReset;
const _SettingsTab({this.onReset});
Widget _buildSettingsTile(BuildContext context, IconData icon, String title, {VoidCallback? onTap}) {
return ListTile(
leading: Icon(icon, color: AppColors.charcoal),
title: Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.charcoal,
),
),
trailing: Icon(Icons.chevron_right, color: AppColors.lightGray),
onTap: onTap ?? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Settings coming soon!')),
);
},
);
}
Future<void> _resetApp(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Reset App?'),
content: const Text('This will clear all data and return you to onboarding. Are you sure?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Reset', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
await ref.read(userProfileProvider.notifier).clearProfile();
await ref.read(cycleEntriesProvider.notifier).clearEntries();
if (context.mounted) {
onReset?.call();
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProfileProvider);
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 24),
// Profile Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
user?.name.isNotEmpty == true ? user!.name[0].toUpperCase() : '?',
style: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.name ?? 'Guest',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
Text(
user?.role == UserRole.husband
? 'HUSBAND'
: (user?.relationshipStatus.name.toUpperCase() ?? 'SINGLE'),
style: GoogleFonts.outfit(
fontSize: 12,
letterSpacing: 1,
color: AppColors.warmGray,
),
),
],
),
),
Icon(Icons.chevron_right, color: AppColors.warmGray),
],
),
),
const SizedBox(height: 24),
// Settings Groups
_buildSettingsGroup('Preferences', [
_buildSettingsTile(context, Icons.notifications_outlined, 'Notifications'),
_buildSettingsTile(context, Icons.palette_outlined, 'Appearance'),
_buildSettingsTile(context, Icons.lock_outline, 'Privacy'),
]),
const SizedBox(height: 16),
_buildSettingsGroup('Cycle', [
_buildSettingsTile(context, Icons.calendar_today_outlined, 'Cycle Settings'),
_buildSettingsTile(context, Icons.trending_up_outlined, 'Cycle History'),
_buildSettingsTile(context, Icons.download_outlined, 'Export Data'),
]),
const SizedBox(height: 16),
_buildSettingsGroup('Account', [
_buildSettingsTile(
context,
Icons.logout,
'Reset App / Logout',
onTap: () => _resetApp(context, ref)
),
]),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildSettingsGroup(String title, List<Widget> tiles) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: tiles,
),
),
],
);
}
}

View File

@@ -0,0 +1,831 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../theme/app_theme.dart';
import '../../models/cycle_entry.dart';
import '../../models/scripture.dart';
import '../../providers/user_provider.dart';
import '../../services/cycle_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Husband's companion app main screen
class HusbandHomeScreen extends ConsumerStatefulWidget {
const HusbandHomeScreen({super.key});
@override
ConsumerState<HusbandHomeScreen> createState() => _HusbandHomeScreenState();
}
class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Theme(
data: _husbandTheme,
child: Scaffold(
backgroundColor: AppColors.warmCream,
body: IndexedStack(
index: _selectedIndex,
children: [
const _HusbandDashboard(),
const _HusbandTipsScreen(),
const _HusbandLearnScreen(),
const _HusbandSettingsScreen(),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: AppColors.navyBlue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
backgroundColor: Colors.white,
selectedItemColor: AppColors.navyBlue,
unselectedItemColor: AppColors.warmGray,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.lightbulb_outline),
activeIcon: Icon(Icons.lightbulb),
label: 'Tips',
),
BottomNavigationBarItem(
icon: Icon(Icons.school_outlined),
activeIcon: Icon(Icons.school),
label: 'Learn',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings',
),
],
),
),
),
);
}
ThemeData get _husbandTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
scaffoldBackgroundColor: AppColors.warmCream,
colorScheme: const ColorScheme.light(
primary: AppColors.navyBlue,
secondary: AppColors.gold,
surface: AppColors.warmCream,
),
appBarTheme: AppBarTheme(
backgroundColor: AppColors.warmCream,
foregroundColor: AppColors.navyBlue,
elevation: 0,
titleTextStyle: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
),
);
}
}
class _HusbandDashboard extends ConsumerWidget {
const _HusbandDashboard();
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProfileProvider);
final cycleInfo = ref.watch(currentCycleInfoProvider);
final wifeName = user?.partnerName ?? "Wife";
final phase = cycleInfo['phase'] as CyclePhase;
final dayOfCycle = cycleInfo['dayOfCycle'] as int;
final daysUntilPeriod = cycleInfo['daysUntilPeriod'] as int;
final scripture = ScriptureDatabase.getHusbandScripture();
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Greeting
Text(
'Hey there,',
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.warmGray,
),
),
Text(
'Husband',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 24),
// Wife's Cycle Status
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.navyBlue,
AppColors.steelBlue,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.favorite,
color: Colors.white,
size: 22,
),
),
const SizedBox(width: 12),
Text(
'$wifeName\'s Cycle',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white.withOpacity(0.9),
),
),
],
),
const SizedBox(height: 16),
Text(
'Day $dayOfCycle${phase.label}',
style: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
daysUntilPeriod > 0
? '~$daysUntilPeriod days until period'
: 'Period expected soon',
style: GoogleFonts.outfit(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(phase.emoji),
const SizedBox(width: 6),
Text(
_getPhaseHint(phase),
style: GoogleFonts.outfit(
fontSize: 12,
color: Colors.white,
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Support Tip
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.navyBlue.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.gold.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.lightbulb_outline,
color: AppColors.gold,
size: 20,
),
),
const SizedBox(width: 12),
Text(
'How to Support Her',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
],
),
const SizedBox(height: 12),
Text(
_getSupportTip(phase),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
height: 1.5,
),
),
],
),
),
const SizedBox(height: 20),
// Scripture for Husbands
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.gold.withOpacity(0.15),
AppColors.warmCream,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.gold.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.menu_book,
color: AppColors.gold,
size: 20,
),
const SizedBox(width: 8),
Text(
'Scripture for Husbands',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
const SizedBox(height: 12),
Text(
'"${scripture.verse}"',
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
color: AppColors.navyBlue,
height: 1.6,
),
),
const SizedBox(height: 8),
Text(
'${scripture.reference}',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
),
const SizedBox(height: 20),
// Prayer Button
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _showPrayerPrompt(context, phase),
icon: const Text('🙏', style: TextStyle(fontSize: 18)),
label: Text(
'Pray for ${wifeName}',
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.navyBlue,
side: const BorderSide(color: AppColors.navyBlue),
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
),
const SizedBox(height: 40),
],
),
),
);
}
String _getPhaseHint(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return 'She may need extra rest';
case CyclePhase.follicular:
return 'Energy is returning';
case CyclePhase.ovulation:
return 'Fertile window';
case CyclePhase.luteal:
return 'PMS may occur';
}
}
String _getSupportTip(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return 'This is a time when she needs extra care. Help with household tasks without being asked. '
'Bring her favorite warm drink, suggest low-key activities, and be extra patient.';
case CyclePhase.follicular:
return 'Her energy is returning! This is a great time to plan dates, work on projects together, '
'and affirm her strengths. She may be more talkative and social.';
case CyclePhase.ovulation:
return 'Prioritize connection time. Romance and quality time matter. '
'If you\'re trying to conceive, this is your fertile window.';
case CyclePhase.luteal:
return 'Be patient—PMS may affect her mood. Listen more, "fix" less. '
'Take initiative on responsibilities and surprise her with comfort foods.';
}
}
void _showPrayerPrompt(BuildContext context, CyclePhase phase) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'🙏 Prayer for Your Wife',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 20),
Text(
_getPrayer(phase),
textAlign: TextAlign.center,
style: GoogleFonts.lora(
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
child: const Text('Amen'),
),
),
const SizedBox(height: 16),
],
),
),
);
}
String _getPrayer(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return '"Lord, I lift up my wife during this time of rest. '
'Give her body the renewal it needs and grant her Your peace. '
'Help me to serve her with patience and love. Amen."';
case CyclePhase.follicular:
return '"Father, thank You for my wife\'s renewed energy. '
'Bless her endeavors and help me to encourage and support her. '
'May our partnership glorify You. Amen."';
case CyclePhase.ovulation:
return '"Creator God, You have designed my wife fearfully and wonderfully. '
'Whatever Your plans for our family, help us trust Your timing. '
'Bless our marriage and intimacy. Amen."';
case CyclePhase.luteal:
return '"Lord, be near to my wife during this phase. '
'When emotions are difficult, grant her Your peace that passes understanding. '
'Help me to be patient, kind, and understanding. Amen."';
}
}
}
class _HusbandTipsScreen extends StatelessWidget {
const _HusbandTipsScreen();
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Supporting Her',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 8),
Text(
'Practical ways to love your wife well',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 24),
_buildTipCategory('During Her Period', [
'🏠 Help with household tasks without being asked',
'🍵 Bring her favorite comfort drink',
'📺 Suggest low-key activities (movies, quiet time)',
'🙏 Pray for her physical comfort',
]),
const SizedBox(height: 16),
_buildTipCategory('Follicular Phase', [
'🎉 Plan dates or activities—her energy is returning',
'💬 She may be more talkative and social',
'💪 Great time for projects together',
'❤️ Affirm her strengths and beauty',
]),
const SizedBox(height: 16),
_buildTipCategory('Luteal Phase (PMS)', [
'😌 Be patient—PMS may affect her mood',
'🍫 Surprise with comfort foods',
'🧹 Take initiative on responsibilities',
'👂 Listen more, "fix" less',
]),
const SizedBox(height: 16),
_buildTipCategory('General Wisdom', [
'🗣️ Ask how she\'s feeling—and actually listen',
'📱 Put your phone down when she\'s talking',
'🌹 Small gestures matter more than grand ones',
'🙏 Pray for her daily',
]),
],
),
),
);
}
Widget _buildTipCategory(String title, List<String> tips) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 12),
...tips.map((tip) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
tip,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
height: 1.4,
),
),
)),
],
),
);
}
}
class _HusbandLearnScreen extends StatelessWidget {
const _HusbandLearnScreen();
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Learn',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 24),
_buildSection('Understanding Her', [
_LearnItem(
icon: Icons.loop,
title: 'The 4 Phases of Her Cycle',
subtitle: 'What\'s happening in her body each month',
),
_LearnItem(
icon: Icons.psychology_outlined,
title: 'Why Does Her Mood Change?',
subtitle: 'Hormones explained simply',
),
_LearnItem(
icon: Icons.medical_information_outlined,
title: 'PMS is Real',
subtitle: 'Medical facts for supportive husbands',
),
]),
const SizedBox(height: 24),
_buildSection('Biblical Manhood', [
_LearnItem(
icon: Icons.favorite,
title: 'Loving Like Christ',
subtitle: 'Ephesians 5 in daily practice',
),
_LearnItem(
icon: Icons.handshake,
title: 'Servant Leadership at Home',
subtitle: 'What it really means',
),
_LearnItem(
icon: Icons.auto_awesome,
title: 'Praying for Your Wife',
subtitle: 'Practical guide',
),
]),
const SizedBox(height: 24),
_buildSection('NFP for Husbands', [
_LearnItem(
icon: Icons.show_chart,
title: 'Reading the Charts Together',
subtitle: 'Understanding fertility signs',
),
_LearnItem(
icon: Icons.schedule,
title: 'Abstinence as Spiritual Discipline',
subtitle: 'Growing together during fertile days',
),
]),
],
),
),
);
}
Widget _buildSection(String title, List<_LearnItem> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: items
.map((item) => ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.navyBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
item.icon,
color: AppColors.navyBlue,
size: 20,
),
),
title: Text(
item.title,
style: GoogleFonts.outfit(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
),
subtitle: Text(
item.subtitle,
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
),
),
trailing: const Icon(
Icons.chevron_right,
color: AppColors.lightGray,
),
onTap: () {},
))
.toList(),
),
),
],
);
}
}
class _LearnItem {
final IconData icon;
final String title;
final String subtitle;
const _LearnItem({
required this.icon,
required this.title,
required this.subtitle,
});
}
class _HusbandSettingsScreen extends ConsumerWidget {
const _HusbandSettingsScreen();
Future<void> _resetApp(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Reset App?'),
content: const Text('This will clear all data and return you to onboarding. Are you sure?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Reset', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
await ref.read(userProfileProvider.notifier).clearProfile();
await ref.read(cycleEntriesProvider.notifier).clearEntries();
if (context.mounted) {
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
const SizedBox(height: 24),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: Text('Notifications', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.link_outlined),
title: Text('Connection', style: GoogleFonts.outfit()),
subtitle: Text('Linked with wife\'s app', style: GoogleFonts.outfit(fontSize: 12)),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.logout),
title: Text('Reset App / Logout', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () => _resetApp(context, ref),
),
ListTile(
leading: const Icon(Icons.help_outline),
title: Text('Help & Support', style: GoogleFonts.outfit()),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,450 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/cycle_entry.dart';
import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart';
import 'package:uuid/uuid.dart';
class LogScreen extends ConsumerStatefulWidget {
const LogScreen({super.key});
@override
ConsumerState<LogScreen> createState() => _LogScreenState();
}
class _LogScreenState extends ConsumerState<LogScreen> {
bool _isPeriodDay = false;
FlowIntensity? _flowIntensity;
MoodLevel? _mood;
int _energyLevel = 3;
int _crampIntensity = 0;
bool _hasHeadache = false;
bool _hasBloating = false;
bool _hasBreastTenderness = false;
bool _hasFatigue = false;
bool _hasAcne = false;
final TextEditingController _notesController = TextEditingController();
@override
void dispose() {
_notesController.dispose();
super.dispose();
}
Future<void> _saveEntry() async {
final entry = CycleEntry(
id: const Uuid().v4(),
date: DateTime.now(),
isPeriodDay: _isPeriodDay,
flowIntensity: _isPeriodDay ? _flowIntensity : null,
mood: _mood,
energyLevel: _energyLevel,
crampIntensity: _crampIntensity > 0 ? _crampIntensity : null,
hasHeadache: _hasHeadache,
hasBloating: _hasBloating,
hasBreastTenderness: _hasBreastTenderness,
hasFatigue: _hasFatigue,
hasAcne: _hasAcne,
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Entry saved!', style: GoogleFonts.outfit()),
backgroundColor: AppColors.sageGreen,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
_resetForm();
}
}
void _resetForm() {
setState(() {
_isPeriodDay = false;
_flowIntensity = null;
_mood = null;
_energyLevel = 3;
_crampIntensity = 0;
_hasHeadache = false;
_hasBloating = false;
_hasBreastTenderness = false;
_hasFatigue = false;
_hasAcne = false;
_notesController.clear();
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Text(
'How are you feeling?',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
Text(
_formatDate(DateTime.now()),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 24),
// Period Toggle
_buildSectionCard(
title: 'Period',
child: Row(
children: [
Expanded(
child: Text(
'Is today a period day?',
style: GoogleFonts.outfit(
fontSize: 16,
color: AppColors.charcoal,
),
),
),
Switch(
value: _isPeriodDay,
onChanged: (value) => setState(() => _isPeriodDay = value),
activeColor: AppColors.menstrualPhase,
),
],
),
),
// Flow Intensity (only if period day)
if (_isPeriodDay) ...[
const SizedBox(height: 16),
_buildSectionCard(
title: 'Flow Intensity',
child: Row(
children: FlowIntensity.values.map((flow) {
final isSelected = _flowIntensity == flow;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _flowIntensity = flow),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected
? AppColors.menstrualPhase.withOpacity(0.2)
: AppColors.lightGray.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border: isSelected
? Border.all(color: AppColors.menstrualPhase)
: null,
),
child: Column(
children: [
Icon(
Icons.water_drop,
color: isSelected
? AppColors.menstrualPhase
: AppColors.warmGray,
size: 20,
),
const SizedBox(height: 4),
Text(
flow.label,
style: GoogleFonts.outfit(
fontSize: 11,
color: isSelected
? AppColors.menstrualPhase
: AppColors.warmGray,
),
),
],
),
),
),
);
}).toList(),
),
),
],
const SizedBox(height: 16),
// Mood
_buildSectionCard(
title: 'Mood',
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: MoodLevel.values.map((mood) {
final isSelected = _mood == mood;
return GestureDetector(
onTap: () => setState(() => _mood = mood),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? AppColors.softGold.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: isSelected
? Border.all(color: AppColors.softGold)
: null,
),
child: Column(
children: [
Text(
mood.emoji,
style: TextStyle(
fontSize: isSelected ? 32 : 28,
),
),
const SizedBox(height: 4),
Text(
mood.label,
style: GoogleFonts.outfit(
fontSize: 10,
color: isSelected
? AppColors.softGold
: AppColors.warmGray,
),
),
],
),
),
);
}).toList(),
),
),
const SizedBox(height: 16),
// Energy Level
_buildSectionCard(
title: 'Energy Level',
child: Column(
children: [
Row(
children: [
const Icon(Icons.battery_1_bar, color: AppColors.warmGray),
Expanded(
child: Slider(
value: _energyLevel.toDouble(),
min: 1,
max: 5,
divisions: 4,
onChanged: (value) {
setState(() => _energyLevel = value.round());
},
),
),
const Icon(Icons.battery_full, color: AppColors.sageGreen),
],
),
Text(
_getEnergyLabel(_energyLevel),
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
),
),
],
),
),
const SizedBox(height: 16),
// Symptoms
_buildSectionCard(
title: 'Symptoms',
child: Column(
children: [
// Cramps Slider
Row(
children: [
SizedBox(
width: 80,
child: Text(
'Cramps',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
),
),
Expanded(
child: Slider(
value: _crampIntensity.toDouble(),
min: 0,
max: 5,
divisions: 5,
activeColor: AppColors.rose,
onChanged: (value) {
setState(() => _crampIntensity = value.round());
},
),
),
SizedBox(
width: 40,
child: Text(
_crampIntensity == 0 ? 'None' : '$_crampIntensity/5',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
),
),
),
],
),
const SizedBox(height: 12),
// Symptom Toggles
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildSymptomChip('Headache', _hasHeadache, (v) => setState(() => _hasHeadache = v)),
_buildSymptomChip('Bloating', _hasBloating, (v) => setState(() => _hasBloating = v)),
_buildSymptomChip('Breast Tenderness', _hasBreastTenderness, (v) => setState(() => _hasBreastTenderness = v)),
_buildSymptomChip('Fatigue', _hasFatigue, (v) => setState(() => _hasFatigue = v)),
_buildSymptomChip('Acne', _hasAcne, (v) => setState(() => _hasAcne = v)),
],
),
],
),
),
const SizedBox(height: 16),
// Notes
_buildSectionCard(
title: 'Notes',
child: TextField(
controller: _notesController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Add any notes about how you\'re feeling...',
hintStyle: GoogleFonts.outfit(
color: AppColors.lightGray,
fontSize: 14,
),
border: InputBorder.none,
),
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.charcoal,
),
),
),
const SizedBox(height: 24),
// Save Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _saveEntry,
child: const Text('Save Entry'),
),
),
const SizedBox(height: 40),
],
),
),
);
}
Widget _buildSectionCard({required String title, required Widget child}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 12),
child,
],
),
);
}
Widget _buildSymptomChip(String label, bool isSelected, ValueChanged<bool> onChanged) {
return GestureDetector(
onTap: () => onChanged(!isSelected),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColors.lavender.withOpacity(0.3) : AppColors.lightGray.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: isSelected ? Border.all(color: AppColors.lavender) : null,
),
child: Text(
label,
style: GoogleFonts.outfit(
fontSize: 13,
color: isSelected ? AppColors.ovulationPhase : AppColors.warmGray,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
),
),
),
);
}
String _formatDate(DateTime date) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
return '${days[date.weekday - 1]}, ${months[date.month - 1]} ${date.day}';
}
String _getEnergyLabel(int level) {
switch (level) {
case 1:
return 'Very Low';
case 2:
return 'Low';
case 3:
return 'Normal';
case 4:
return 'Good';
case 5:
return 'Excellent';
default:
return 'Normal';
}
}
}

View File

@@ -0,0 +1,642 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart';
import '../../theme/app_theme.dart';
import 'package:christian_period_tracker/models/user_profile.dart';
import 'package:christian_period_tracker/models/cycle_entry.dart';
import '../home/home_screen.dart';
import '../husband/husband_home_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/user_provider.dart';
class OnboardingScreen extends ConsumerStatefulWidget {
const OnboardingScreen({super.key});
@override
ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
final PageController _pageController = PageController();
int _currentPage = 0;
bool _isNavigating = false; // Debounce flag
// Form data
UserRole _role = UserRole.wife;
String _name = '';
RelationshipStatus _relationshipStatus = RelationshipStatus.single;
FertilityGoal? _fertilityGoal;
int _averageCycleLength = 28;
DateTime? _lastPeriodStart;
bool _isIrregularCycle = false;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _nextPage() async {
if (_isNavigating) return;
_isNavigating = true;
// Husband Flow: Role (0) -> Name (1) -> Finish
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4)
int nextPage = _currentPage + 1;
// Logic for skipping pages
if (_role == UserRole.husband) {
if (_currentPage == 1) {
await _completeOnboarding();
return;
}
} else {
// Wife flow
if (_currentPage == 2 && _relationshipStatus != RelationshipStatus.married) {
// Skip fertility goal (page 3) if not married
nextPage = 4;
}
}
if (nextPage <= 4) { // Max pages
await _pageController.animateToPage(
nextPage,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
} else {
await _completeOnboarding();
}
// Reset debounce after animation
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) setState(() => _isNavigating = false);
});
}
void _previousPage() async {
if (_isNavigating) return;
_isNavigating = true;
int prevPage = _currentPage - 1;
// Logic for reverse skipping
if (_role == UserRole.wife) {
if (_currentPage == 4 && _relationshipStatus != RelationshipStatus.married) {
// Skip back over fertility goal (page 3)
prevPage = 2;
}
}
if (prevPage >= 0) {
await _pageController.animateToPage(
prevPage,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
// Reset debounce after animation
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) setState(() => _isNavigating = false);
});
}
Future<void> _completeOnboarding() async {
final userProfile = UserProfile(
id: const Uuid().v4(),
name: _name,
role: _role,
relationshipStatus: _role == UserRole.husband ? RelationshipStatus.married : _relationshipStatus,
fertilityGoal: (_role == UserRole.wife && _relationshipStatus == RelationshipStatus.married) ? _fertilityGoal : null,
averageCycleLength: _averageCycleLength,
lastPeriodStartDate: _lastPeriodStart,
isIrregularCycle: _isIrregularCycle,
hasCompletedOnboarding: true,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await ref.read(userProfileProvider.notifier).updateProfile(userProfile);
if (mounted) {
// Navigate to appropriate home screen
if (_role == UserRole.husband) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const HusbandHomeScreen(),
),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
}
}
}
@override
Widget build(BuildContext context) {
// Different background color for husband flow
final bgColor = _role == UserRole.husband ? AppColors.warmCream : AppColors.cream;
return Scaffold(
backgroundColor: bgColor,
body: SafeArea(
child: Column(
children: [
// Progress indicator (hide on role page 0)
if (_currentPage > 0)
Padding(
padding: const EdgeInsets.all(24),
child: SmoothPageIndicator(
controller: _pageController,
count: _role == UserRole.husband ? 2 : 5,
effect: WormEffect(
dotHeight: 8,
dotWidth: 8,
spacing: 12,
activeDotColor: _role == UserRole.husband ? AppColors.navyBlue : AppColors.sageGreen,
dotColor: AppColors.lightGray.withOpacity(0.3),
),
),
),
// Pages
Expanded(
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(), // Disable swipe
onPageChanged: (index) {
setState(() => _currentPage = index);
},
children: [
_buildRolePage(), // Page 0
_buildNamePage(), // Page 1
_buildRelationshipPage(), // Page 2 (Wife only)
_buildFertilityGoalPage(), // Page 3 (Wife married only)
_buildCyclePage(), // Page 4 (Wife only)
],
),
),
],
),
),
);
}
Widget _buildRolePage() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.favorite_rounded,
size: 40,
color: Colors.white,
),
),
const SizedBox(height: 32),
Text(
'Who is this app for?',
textAlign: TextAlign.center,
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 48),
_buildRoleOption(UserRole.wife, 'For Her', 'Track cycle, health, and faith', Icons.female),
const SizedBox(height: 16),
_buildRoleOption(UserRole.husband, 'For Him', 'Support your wife and grow together', Icons.male),
const Spacer(),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _nextPage,
style: ElevatedButton.styleFrom(
backgroundColor: _role == UserRole.husband ? AppColors.navyBlue : AppColors.sageGreen,
),
child: const Text('Continue'),
),
),
],
),
);
}
Widget _buildRoleOption(UserRole role, String title, String subtitle, IconData icon) {
final isSelected = _role == role;
// Dynamic colors based on role selection
final activeColor = role == UserRole.wife ? AppColors.sageGreen : AppColors.navyBlue;
final activeBg = role == UserRole.wife ? AppColors.sageGreen.withOpacity(0.1) : AppColors.navyBlue.withOpacity(0.1);
return GestureDetector(
onTap: () => setState(() => _role = role),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isSelected ? activeBg : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? activeColor : AppColors.lightGray.withOpacity(0.5),
width: isSelected ? 2 : 1,
),
boxShadow: isSelected ? [
BoxShadow(
color: activeColor.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
)
] : [],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? activeColor : AppColors.lightGray.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: isSelected ? Colors.white : AppColors.warmGray,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
],
),
),
if (isSelected)
Icon(Icons.check_circle, color: activeColor),
],
),
),
);
}
Widget _buildNamePage() {
final isHusband = _role == UserRole.husband;
final activeColor = isHusband ? AppColors.navyBlue : AppColors.sageGreen;
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text(
isHusband ? 'What\'s your name, sir?' : 'What\'s your name?',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: isHusband ? AppColors.navyBlue : AppColors.charcoal,
),
),
const SizedBox(height: 8),
Text(
'We\'ll use this to personalize the app.',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 32),
TextField(
onChanged: (value) => setState(() => _name = value),
decoration: InputDecoration(
hintText: 'Enter your name',
prefixIcon: Icon(
Icons.person_outline,
color: AppColors.warmGray,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: activeColor),
),
),
style: GoogleFonts.outfit(fontSize: 16),
textCapitalization: TextCapitalization.words,
),
const Spacer(),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(
foregroundColor: isHusband ? AppColors.navyBlue : AppColors.sageGreen,
side: BorderSide(color: isHusband ? AppColors.navyBlue : AppColors.sageGreen),
),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: (_name.isNotEmpty && !_isNavigating) ? _nextPage : null,
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
),
child: Text(isHusband ? 'Finish Setup' : 'Continue'),
),
),
],
),
],
),
);
}
Widget _buildRelationshipPage() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text(
'Tell us about yourself',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 32),
_buildRelationshipOption(RelationshipStatus.single, 'Single', 'Wellness focus', Icons.person_outline),
const SizedBox(height: 12),
_buildRelationshipOption(RelationshipStatus.engaged, 'Engaged', 'Prepare for marriage', Icons.favorite_border),
const SizedBox(height: 12),
_buildRelationshipOption(RelationshipStatus.married, 'Married', 'Fertility & intimacy', Icons.favorite),
const Spacer(),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(foregroundColor: AppColors.sageGreen, side: BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: (_relationshipStatus != null && !_isNavigating) ? _nextPage : null,
child: const Text('Continue'),
),
),
],
),
],
),
);
}
Widget _buildRelationshipOption(RelationshipStatus status, String title, String subtitle, IconData icon) {
final isSelected = _relationshipStatus == status;
return GestureDetector(
onTap: () => setState(() => _relationshipStatus = status),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected ? AppColors.sageGreen.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColors.sageGreen : AppColors.lightGray.withOpacity(0.5),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Icon(icon, color: isSelected ? AppColors.sageGreen : AppColors.warmGray),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
Text(subtitle, style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
],
),
),
if (isSelected) Icon(Icons.check_circle, color: AppColors.sageGreen),
],
),
),
);
}
Widget _buildFertilityGoalPage() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text('What\'s your goal?', style: GoogleFonts.outfit(fontSize: 28, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
const SizedBox(height: 32),
_buildGoalOption(FertilityGoal.tryingToConceive, 'Trying to Conceive', 'Track fertile days', Icons.child_care_outlined),
const SizedBox(height: 12),
_buildGoalOption(FertilityGoal.tryingToAvoid, 'Natural Family Planning', 'Track fertility signs', Icons.calendar_today_outlined),
const SizedBox(height: 12),
_buildGoalOption(FertilityGoal.justTracking, 'Just Tracking', 'Monitor cycle health', Icons.insights_outlined),
const Spacer(),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(foregroundColor: AppColors.sageGreen, side: BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: (_fertilityGoal != null && !_isNavigating) ? _nextPage : null,
child: const Text('Continue'),
),
),
],
),
],
),
);
}
Widget _buildGoalOption(FertilityGoal goal, String title, String subtitle, IconData icon) {
final isSelected = _fertilityGoal == goal;
return GestureDetector(
onTap: () => setState(() => _fertilityGoal = goal),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected ? AppColors.sageGreen.withOpacity(0.1) : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? AppColors.sageGreen : AppColors.lightGray.withOpacity(0.5),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
Icon(icon, color: isSelected ? AppColors.sageGreen : AppColors.warmGray),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
Text(subtitle, style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
],
),
),
if (isSelected) Icon(Icons.check_circle, color: AppColors.sageGreen),
],
),
),
);
}
Widget _buildCyclePage() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Text('About your cycle', style: GoogleFonts.outfit(fontSize: 28, fontWeight: FontWeight.w600, color: AppColors.charcoal)),
const SizedBox(height: 32),
Text('Average cycle length', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.charcoal)),
Row(
children: [
Expanded(
child: Slider(
value: _averageCycleLength.toDouble(),
min: 21,
max: 40,
divisions: 19,
onChanged: (value) => setState(() => _averageCycleLength = value.round()),
),
),
Text('$_averageCycleLength days', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
],
),
// Irregular Cycle Checkbox
CheckboxListTile(
title: Text('My cycles are irregular', style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal)),
value: _isIrregularCycle,
onChanged: (val) => setState(() => _isIrregularCycle = val ?? false),
activeColor: AppColors.sageGreen,
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 24),
Text('Last period start date', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.charcoal)),
const SizedBox(height: 8),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: _lastPeriodStart ?? DateTime.now(),
firstDate: DateTime.now().subtract(const Duration(days: 60)),
lastDate: DateTime.now(),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(primary: AppColors.sageGreen, onPrimary: Colors.white, surface: Colors.white, onSurface: AppColors.charcoal),
),
child: child!,
);
},
);
if (date != null) setState(() => _lastPeriodStart = date);
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.lightGray.withOpacity(0.5))),
child: Row(
children: [
Icon(Icons.calendar_today, color: AppColors.warmGray),
const SizedBox(width: 12),
Text(_lastPeriodStart != null ? "${_lastPeriodStart!.month}/${_lastPeriodStart!.day}/${_lastPeriodStart!.year}" : "Select Date", style: GoogleFonts.outfit(fontSize: 16, color: AppColors.charcoal)),
],
),
),
),
const Spacer(),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _previousPage,
style: OutlinedButton.styleFrom(foregroundColor: AppColors.sageGreen, side: BorderSide(color: AppColors.sageGreen)),
child: const Text('Back'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: (_lastPeriodStart != null && !_isNavigating) ? _nextPage : null,
child: const Text('Get Started'),
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import 'onboarding/onboarding_screen.dart';
import 'home/home_screen.dart';
import '../models/user_profile.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/user_provider.dart';
import 'husband/husband_home_screen.dart';
class SplashScreen extends ConsumerStatefulWidget {
const SplashScreen({super.key});
@override
ConsumerState<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends ConsumerState<SplashScreen> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeOutBack),
),
);
_controller.forward();
// Navigate after splash
Future.delayed(const Duration(milliseconds: 2500), () {
_navigateToNextScreen();
});
}
void _navigateToNextScreen() {
final user = ref.read(userProfileProvider);
final hasProfile = user != null;
Widget nextScreen;
if (!hasProfile) {
nextScreen = const OnboardingScreen();
} else if (user.role == UserRole.husband) {
nextScreen = const HusbandHomeScreen();
} else {
nextScreen = const HomeScreen();
}
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => nextScreen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 500),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.cream,
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// App Icon/Logo
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.blushPink,
AppColors.rose.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: AppColors.rose.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.favorite_rounded,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 24),
// App Name placeholder
Text(
'Period Tracker',
style: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 8),
// Tagline
Text(
'Faith-Centered Wellness',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.warmGray,
letterSpacing: 1.2,
),
),
const SizedBox(height: 48),
// Scripture
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: Text(
'"I praise you because I am\nfearfully and wonderfully made."',
textAlign: TextAlign.center,
style: GoogleFonts.lora(
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.5,
),
),
),
const SizedBox(height: 8),
Text(
'— Psalm 139:14',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
),
);
},
),
),
);
}
}