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

49
lib/main.dart Normal file
View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
import 'theme/app_theme.dart';
import 'screens/splash_screen.dart';
import 'models/user_profile.dart';
import 'models/cycle_entry.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive for local storage
await Hive.initFlutter();
// Register Hive adapters
Hive.registerAdapter(UserProfileAdapter());
Hive.registerAdapter(CycleEntryAdapter());
Hive.registerAdapter(RelationshipStatusAdapter());
Hive.registerAdapter(FertilityGoalAdapter());
Hive.registerAdapter(MoodLevelAdapter());
Hive.registerAdapter(FlowIntensityAdapter());
Hive.registerAdapter(CervicalMucusTypeAdapter());
Hive.registerAdapter(CyclePhaseAdapter());
Hive.registerAdapter(UserRoleAdapter());
// Open boxes
await Hive.openBox<UserProfile>('user_profile');
await Hive.openBox<CycleEntry>('cycle_entries');
runApp(const ProviderScope(child: ChristianPeriodTrackerApp()));
}
class ChristianPeriodTrackerApp extends StatelessWidget {
const ChristianPeriodTrackerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Christian Period Tracker',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const SplashScreen(),
);
}
}

335
lib/models/cycle_entry.dart Normal file
View File

@@ -0,0 +1,335 @@
import 'package:hive/hive.dart';
part 'cycle_entry.g.dart';
/// Mood levels for tracking
@HiveType(typeId: 3)
enum MoodLevel {
@HiveField(0)
verySad,
@HiveField(1)
sad,
@HiveField(2)
neutral,
@HiveField(3)
happy,
@HiveField(4)
veryHappy,
}
/// Flow intensity for period days
@HiveType(typeId: 4)
enum FlowIntensity {
@HiveField(0)
spotting,
@HiveField(1)
light,
@HiveField(2)
medium,
@HiveField(3)
heavy,
}
/// Cervical mucus type for NFP tracking
@HiveType(typeId: 5)
enum CervicalMucusType {
@HiveField(0)
dry,
@HiveField(1)
sticky,
@HiveField(2)
creamy,
@HiveField(3)
eggWhite,
@HiveField(4)
watery,
}
/// Cycle phase
@HiveType(typeId: 6)
enum CyclePhase {
@HiveField(0)
menstrual,
@HiveField(1)
follicular,
@HiveField(2)
ovulation,
@HiveField(3)
luteal,
}
/// Daily cycle entry
@HiveType(typeId: 7)
class CycleEntry extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
DateTime date;
@HiveField(2)
bool isPeriodDay;
@HiveField(3)
FlowIntensity? flowIntensity;
@HiveField(4)
MoodLevel? mood;
@HiveField(5)
int? energyLevel; // 1-5
@HiveField(6)
int? crampIntensity; // 1-5
@HiveField(7)
bool hasHeadache;
@HiveField(8)
bool hasBloating;
@HiveField(9)
bool hasBreastTenderness;
@HiveField(10)
bool hasFatigue;
@HiveField(11)
bool hasAcne;
@HiveField(12)
double? basalBodyTemperature; // in Fahrenheit
@HiveField(13)
CervicalMucusType? cervicalMucus;
@HiveField(14)
bool? ovulationTestPositive;
@HiveField(15)
String? notes;
@HiveField(16)
int? sleepHours;
@HiveField(17)
int? waterIntake; // glasses
@HiveField(18)
bool hadExercise;
@HiveField(19)
bool hadIntimacy; // For married users only
@HiveField(20)
DateTime createdAt;
@HiveField(21)
DateTime updatedAt;
CycleEntry({
required this.id,
required this.date,
this.isPeriodDay = false,
this.flowIntensity,
this.mood,
this.energyLevel,
this.crampIntensity,
this.hasHeadache = false,
this.hasBloating = false,
this.hasBreastTenderness = false,
this.hasFatigue = false,
this.hasAcne = false,
this.basalBodyTemperature,
this.cervicalMucus,
this.ovulationTestPositive,
this.notes,
this.sleepHours,
this.waterIntake,
this.hadExercise = false,
this.hadIntimacy = false,
required this.createdAt,
required this.updatedAt,
});
/// Check if any symptoms are logged
bool get hasSymptoms =>
hasHeadache ||
hasBloating ||
hasBreastTenderness ||
hasFatigue ||
hasAcne ||
(crampIntensity != null && crampIntensity! > 0);
/// Check if NFP data is logged
bool get hasNFPData =>
basalBodyTemperature != null ||
cervicalMucus != null ||
ovulationTestPositive != null;
/// Get symptom count
int get symptomCount {
int count = 0;
if (hasHeadache) count++;
if (hasBloating) count++;
if (hasBreastTenderness) count++;
if (hasFatigue) count++;
if (hasAcne) count++;
if (crampIntensity != null && crampIntensity! > 0) count++;
return count;
}
/// Copy with updated fields
CycleEntry copyWith({
String? id,
DateTime? date,
bool? isPeriodDay,
FlowIntensity? flowIntensity,
MoodLevel? mood,
int? energyLevel,
int? crampIntensity,
bool? hasHeadache,
bool? hasBloating,
bool? hasBreastTenderness,
bool? hasFatigue,
bool? hasAcne,
double? basalBodyTemperature,
CervicalMucusType? cervicalMucus,
bool? ovulationTestPositive,
String? notes,
int? sleepHours,
int? waterIntake,
bool? hadExercise,
bool? hadIntimacy,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return CycleEntry(
id: id ?? this.id,
date: date ?? this.date,
isPeriodDay: isPeriodDay ?? this.isPeriodDay,
flowIntensity: flowIntensity ?? this.flowIntensity,
mood: mood ?? this.mood,
energyLevel: energyLevel ?? this.energyLevel,
crampIntensity: crampIntensity ?? this.crampIntensity,
hasHeadache: hasHeadache ?? this.hasHeadache,
hasBloating: hasBloating ?? this.hasBloating,
hasBreastTenderness: hasBreastTenderness ?? this.hasBreastTenderness,
hasFatigue: hasFatigue ?? this.hasFatigue,
hasAcne: hasAcne ?? this.hasAcne,
basalBodyTemperature: basalBodyTemperature ?? this.basalBodyTemperature,
cervicalMucus: cervicalMucus ?? this.cervicalMucus,
ovulationTestPositive: ovulationTestPositive ?? this.ovulationTestPositive,
notes: notes ?? this.notes,
sleepHours: sleepHours ?? this.sleepHours,
waterIntake: waterIntake ?? this.waterIntake,
hadExercise: hadExercise ?? this.hadExercise,
hadIntimacy: hadIntimacy ?? this.hadIntimacy,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? DateTime.now(),
);
}
}
/// Extension to get display string for enums
extension MoodLevelExtension on MoodLevel {
String get emoji {
switch (this) {
case MoodLevel.verySad:
return '😢';
case MoodLevel.sad:
return '😕';
case MoodLevel.neutral:
return '😐';
case MoodLevel.happy:
return '🙂';
case MoodLevel.veryHappy:
return '😄';
}
}
String get label {
switch (this) {
case MoodLevel.verySad:
return 'Very Sad';
case MoodLevel.sad:
return 'Sad';
case MoodLevel.neutral:
return 'Neutral';
case MoodLevel.happy:
return 'Happy';
case MoodLevel.veryHappy:
return 'Very Happy';
}
}
}
extension FlowIntensityExtension on FlowIntensity {
String get label {
switch (this) {
case FlowIntensity.spotting:
return 'Spotting';
case FlowIntensity.light:
return 'Light';
case FlowIntensity.medium:
return 'Medium';
case FlowIntensity.heavy:
return 'Heavy';
}
}
}
extension CyclePhaseExtension on CyclePhase {
String get label {
switch (this) {
case CyclePhase.menstrual:
return 'Menstrual';
case CyclePhase.follicular:
return 'Follicular';
case CyclePhase.ovulation:
return 'Ovulation';
case CyclePhase.luteal:
return 'Luteal';
}
}
String get emoji {
switch (this) {
case CyclePhase.menstrual:
return '🩸';
case CyclePhase.follicular:
return '🌱';
case CyclePhase.ovulation:
return '🌸';
case CyclePhase.luteal:
return '🌙';
}
}
String get description {
switch (this) {
case CyclePhase.menstrual:
return 'A time for rest and reflection';
case CyclePhase.follicular:
return 'A time of renewal and energy';
case CyclePhase.ovulation:
return 'Peak fertility window';
case CyclePhase.luteal:
return 'A time for patience and self-care';
}
}
}

View File

@@ -0,0 +1,310 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cycle_entry.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CycleEntryAdapter extends TypeAdapter<CycleEntry> {
@override
final int typeId = 7;
@override
CycleEntry read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CycleEntry(
id: fields[0] as String,
date: fields[1] as DateTime,
isPeriodDay: fields[2] as bool,
flowIntensity: fields[3] as FlowIntensity?,
mood: fields[4] as MoodLevel?,
energyLevel: fields[5] as int?,
crampIntensity: fields[6] as int?,
hasHeadache: fields[7] as bool,
hasBloating: fields[8] as bool,
hasBreastTenderness: fields[9] as bool,
hasFatigue: fields[10] as bool,
hasAcne: fields[11] as bool,
basalBodyTemperature: fields[12] as double?,
cervicalMucus: fields[13] as CervicalMucusType?,
ovulationTestPositive: fields[14] as bool?,
notes: fields[15] as String?,
sleepHours: fields[16] as int?,
waterIntake: fields[17] as int?,
hadExercise: fields[18] as bool,
hadIntimacy: fields[19] as bool,
createdAt: fields[20] as DateTime,
updatedAt: fields[21] as DateTime,
);
}
@override
void write(BinaryWriter writer, CycleEntry obj) {
writer
..writeByte(22)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.date)
..writeByte(2)
..write(obj.isPeriodDay)
..writeByte(3)
..write(obj.flowIntensity)
..writeByte(4)
..write(obj.mood)
..writeByte(5)
..write(obj.energyLevel)
..writeByte(6)
..write(obj.crampIntensity)
..writeByte(7)
..write(obj.hasHeadache)
..writeByte(8)
..write(obj.hasBloating)
..writeByte(9)
..write(obj.hasBreastTenderness)
..writeByte(10)
..write(obj.hasFatigue)
..writeByte(11)
..write(obj.hasAcne)
..writeByte(12)
..write(obj.basalBodyTemperature)
..writeByte(13)
..write(obj.cervicalMucus)
..writeByte(14)
..write(obj.ovulationTestPositive)
..writeByte(15)
..write(obj.notes)
..writeByte(16)
..write(obj.sleepHours)
..writeByte(17)
..write(obj.waterIntake)
..writeByte(18)
..write(obj.hadExercise)
..writeByte(19)
..write(obj.hadIntimacy)
..writeByte(20)
..write(obj.createdAt)
..writeByte(21)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CycleEntryAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class MoodLevelAdapter extends TypeAdapter<MoodLevel> {
@override
final int typeId = 3;
@override
MoodLevel read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return MoodLevel.verySad;
case 1:
return MoodLevel.sad;
case 2:
return MoodLevel.neutral;
case 3:
return MoodLevel.happy;
case 4:
return MoodLevel.veryHappy;
default:
return MoodLevel.verySad;
}
}
@override
void write(BinaryWriter writer, MoodLevel obj) {
switch (obj) {
case MoodLevel.verySad:
writer.writeByte(0);
break;
case MoodLevel.sad:
writer.writeByte(1);
break;
case MoodLevel.neutral:
writer.writeByte(2);
break;
case MoodLevel.happy:
writer.writeByte(3);
break;
case MoodLevel.veryHappy:
writer.writeByte(4);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MoodLevelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class FlowIntensityAdapter extends TypeAdapter<FlowIntensity> {
@override
final int typeId = 4;
@override
FlowIntensity read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return FlowIntensity.spotting;
case 1:
return FlowIntensity.light;
case 2:
return FlowIntensity.medium;
case 3:
return FlowIntensity.heavy;
default:
return FlowIntensity.spotting;
}
}
@override
void write(BinaryWriter writer, FlowIntensity obj) {
switch (obj) {
case FlowIntensity.spotting:
writer.writeByte(0);
break;
case FlowIntensity.light:
writer.writeByte(1);
break;
case FlowIntensity.medium:
writer.writeByte(2);
break;
case FlowIntensity.heavy:
writer.writeByte(3);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlowIntensityAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class CervicalMucusTypeAdapter extends TypeAdapter<CervicalMucusType> {
@override
final int typeId = 5;
@override
CervicalMucusType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return CervicalMucusType.dry;
case 1:
return CervicalMucusType.sticky;
case 2:
return CervicalMucusType.creamy;
case 3:
return CervicalMucusType.eggWhite;
case 4:
return CervicalMucusType.watery;
default:
return CervicalMucusType.dry;
}
}
@override
void write(BinaryWriter writer, CervicalMucusType obj) {
switch (obj) {
case CervicalMucusType.dry:
writer.writeByte(0);
break;
case CervicalMucusType.sticky:
writer.writeByte(1);
break;
case CervicalMucusType.creamy:
writer.writeByte(2);
break;
case CervicalMucusType.eggWhite:
writer.writeByte(3);
break;
case CervicalMucusType.watery:
writer.writeByte(4);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CervicalMucusTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class CyclePhaseAdapter extends TypeAdapter<CyclePhase> {
@override
final int typeId = 6;
@override
CyclePhase read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return CyclePhase.menstrual;
case 1:
return CyclePhase.follicular;
case 2:
return CyclePhase.ovulation;
case 3:
return CyclePhase.luteal;
default:
return CyclePhase.menstrual;
}
}
@override
void write(BinaryWriter writer, CyclePhase obj) {
switch (obj) {
case CyclePhase.menstrual:
writer.writeByte(0);
break;
case CyclePhase.follicular:
writer.writeByte(1);
break;
case CyclePhase.ovulation:
writer.writeByte(2);
break;
case CyclePhase.luteal:
writer.writeByte(3);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CyclePhaseAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

235
lib/models/scripture.dart Normal file
View File

@@ -0,0 +1,235 @@
/// Scripture model for daily verses and devotionals
class Scripture {
final String verse;
final String reference;
final String? reflection;
final List<String> applicablePhases;
final List<String> applicableContexts;
const Scripture({
required this.verse,
required this.reference,
this.reflection,
this.applicablePhases = const [],
this.applicableContexts = const [],
});
}
/// Pre-defined scriptures for the app
class ScriptureDatabase {
/// Scriptures for menstrual phase (rest, comfort)
static const List<Scripture> menstrualScriptures = [
Scripture(
verse: "Come to me, all you who are weary and burdened, and I will give you rest.",
reference: "Matthew 11:28",
reflection: "Your body is doing important work. Rest is not weakness—it's wisdom.",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "He gives strength to the weary and increases the power of the weak.",
reference: "Isaiah 40:29",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "The Lord is my shepherd; I shall not want. He makes me lie down in green pastures.",
reference: "Psalm 23:1-2",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "Be still, and know that I am God.",
reference: "Psalm 46:10",
reflection: "Use this time of slowing down to be present with God.",
applicablePhases: ['menstrual'],
),
Scripture(
verse: "My grace is sufficient for you, for my power is made perfect in weakness.",
reference: "2 Corinthians 12:9",
applicablePhases: ['menstrual'],
),
];
/// Scriptures for follicular phase (renewal, strength)
static const List<Scripture> follicularScriptures = [
Scripture(
verse: "She is clothed with strength and dignity; she can laugh at the days to come.",
reference: "Proverbs 31:25",
reflection: "You're entering a season of renewed energy. Use it for His glory.",
applicablePhases: ['follicular'],
),
Scripture(
verse: "I can do all this through him who gives me strength.",
reference: "Philippians 4:13",
applicablePhases: ['follicular'],
),
Scripture(
verse: "But those who hope in the Lord will renew their strength. They will soar on wings like eagles.",
reference: "Isaiah 40:31",
applicablePhases: ['follicular'],
),
Scripture(
verse: "This is the day the Lord has made; let us rejoice and be glad in it.",
reference: "Psalm 118:24",
applicablePhases: ['follicular'],
),
Scripture(
verse: "The Lord your God is with you, the Mighty Warrior who saves.",
reference: "Zephaniah 3:17",
applicablePhases: ['follicular'],
),
];
/// Scriptures for ovulation phase (creation, beauty)
static const List<Scripture> ovulationScriptures = [
Scripture(
verse: "For you created my inmost being; you knit me together in my mother's womb. I praise you because I am fearfully and wonderfully made.",
reference: "Psalm 139:13-14",
reflection: "Your body reflects the incredible creativity of God.",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "Children are a heritage from the Lord, offspring a reward from him.",
reference: "Psalm 127:3",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "See, I am doing a new thing! Now it springs up; do you not perceive it?",
reference: "Isaiah 43:19",
applicablePhases: ['ovulation'],
),
Scripture(
verse: "Every good and perfect gift is from above.",
reference: "James 1:17",
applicablePhases: ['ovulation'],
),
];
/// Scriptures for luteal phase / TWW (patience, trust)
static const List<Scripture> lutealScriptures = [
Scripture(
verse: "For I know the plans I have for you, declares the Lord, plans to prosper you and not to harm you, plans to give you hope and a future.",
reference: "Jeremiah 29:11",
reflection: "Whatever this season holds, God's plans for you are good.",
applicablePhases: ['luteal'],
),
Scripture(
verse: "Do not be anxious about anything, but in every situation, by prayer and petition, with thanksgiving, present your requests to God.",
reference: "Philippians 4:6",
applicablePhases: ['luteal'],
),
Scripture(
verse: "Trust in the Lord with all your heart and lean not on your own understanding.",
reference: "Proverbs 3:5",
applicablePhases: ['luteal'],
),
Scripture(
verse: "The Lord is close to the brokenhearted and saves those who are crushed in spirit.",
reference: "Psalm 34:18",
applicablePhases: ['luteal'],
),
Scripture(
verse: "And the peace of God, which transcends all understanding, will guard your hearts and your minds in Christ Jesus.",
reference: "Philippians 4:7",
applicablePhases: ['luteal'],
),
Scripture(
verse: "Wait for the Lord; be strong and take heart and wait for the Lord.",
reference: "Psalm 27:14",
applicablePhases: ['luteal'],
),
];
/// Scriptures for husbands
static const List<Scripture> husbandScriptures = [
Scripture(
verse: "Husbands, love your wives, just as Christ loved the church and gave himself up for her.",
reference: "Ephesians 5:25",
reflection: "Love sacrificially—putting her needs before your own.",
),
Scripture(
verse: "Husbands, in the same way be considerate as you live with your wives, and treat them with respect.",
reference: "1 Peter 3:7",
),
Scripture(
verse: "Two are better than one, because they have a good return for their labor.",
reference: "Ecclesiastes 4:9",
),
Scripture(
verse: "Be completely humble and gentle; be patient, bearing with one another in love.",
reference: "Ephesians 4:2",
),
Scripture(
verse: "Above all, love each other deeply, because love covers over a multitude of sins.",
reference: "1 Peter 4:8",
),
Scripture(
verse: "A husband should fulfill his duty to his wife.",
reference: "1 Corinthians 7:3",
),
Scripture(
verse: "He who finds a wife finds what is good and receives favor from the Lord.",
reference: "Proverbs 18:22",
),
];
/// General womanhood scriptures
static const List<Scripture> womanhoodScriptures = [
Scripture(
verse: "Charm is deceptive, and beauty is fleeting; but a woman who fears the Lord is to be praised.",
reference: "Proverbs 31:30",
),
Scripture(
verse: "She opens her mouth with wisdom, and the teaching of kindness is on her tongue.",
reference: "Proverbs 31:26",
),
Scripture(
verse: "Your beauty should not come from outward adornment... Rather, it should be that of your inner self, the unfading beauty of a gentle and quiet spirit.",
reference: "1 Peter 3:3-4",
),
Scripture(
verse: "God is within her, she will not fall; God will help her at break of day.",
reference: "Psalm 46:5",
),
];
/// Get scripture for current phase
static Scripture getScriptureForPhase(String phase) {
final List<Scripture> scriptures;
switch (phase.toLowerCase()) {
case 'menstrual':
scriptures = menstrualScriptures;
break;
case 'follicular':
scriptures = follicularScriptures;
break;
case 'ovulation':
scriptures = ovulationScriptures;
break;
case 'luteal':
scriptures = lutealScriptures;
break;
default:
scriptures = [...menstrualScriptures, ...follicularScriptures, ...ovulationScriptures, ...lutealScriptures];
}
// Return a scripture based on the day of year for variety
final dayOfYear = DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays;
return scriptures[dayOfYear % scriptures.length];
}
/// Get scripture for husband
static Scripture getHusbandScripture() {
final dayOfYear = DateTime.now().difference(DateTime(DateTime.now().year, 1, 1)).inDays;
return husbandScriptures[dayOfYear % husbandScriptures.length];
}
/// Get all scriptures
static List<Scripture> getAllScriptures() {
return [
...menstrualScriptures,
...follicularScriptures,
...ovulationScriptures,
...lutealScriptures,
...womanhoodScriptures,
];
}
}

View File

@@ -0,0 +1,163 @@
import 'package:hive/hive.dart';
part 'user_profile.g.dart';
/// User's relationship status
@HiveType(typeId: 0)
enum RelationshipStatus {
@HiveField(0)
single,
@HiveField(1)
engaged,
@HiveField(2)
married,
}
/// Fertility tracking goal for married users
@HiveType(typeId: 1)
enum FertilityGoal {
@HiveField(0)
tryingToConceive, // TTC
@HiveField(1)
tryingToAvoid, // TTA - NFP
@HiveField(2)
justTracking,
}
/// User profile model
@HiveType(typeId: 2)
class UserProfile extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
String name;
@HiveField(2)
RelationshipStatus relationshipStatus;
@HiveField(3)
FertilityGoal? fertilityGoal;
@HiveField(4)
int averageCycleLength;
@HiveField(5)
int averagePeriodLength;
@HiveField(6)
DateTime? lastPeriodStartDate;
@HiveField(7)
bool notificationsEnabled;
@HiveField(8)
String? devotionalTime; // HH:mm format
@HiveField(9)
bool hasCompletedOnboarding;
@HiveField(10)
DateTime createdAt;
@HiveField(11)
DateTime updatedAt;
@HiveField(12)
String? partnerName; // For married users
@HiveField(14, defaultValue: UserRole.wife)
UserRole role;
@HiveField(15, defaultValue: false)
bool isIrregularCycle;
UserProfile({
required this.id,
required this.name,
this.relationshipStatus = RelationshipStatus.single,
this.fertilityGoal,
this.averageCycleLength = 28,
this.averagePeriodLength = 5,
this.lastPeriodStartDate,
this.notificationsEnabled = true,
this.devotionalTime,
this.hasCompletedOnboarding = false,
required this.createdAt,
required this.updatedAt,
this.partnerName,
this.role = UserRole.wife,
this.isIrregularCycle = false,
});
/// Check if user is married
bool get isMarried => relationshipStatus == RelationshipStatus.married;
/// Check if user is trying to conceive
bool get isTTC => fertilityGoal == FertilityGoal.tryingToConceive;
/// Check if user is practicing NFP
bool get isNFP => fertilityGoal == FertilityGoal.tryingToAvoid;
/// Check if user is husband
bool get isHusband => role == UserRole.husband;
/// Should show fertility content
bool get showFertilityContent =>
!isHusband && isMarried && fertilityGoal != FertilityGoal.justTracking && fertilityGoal != null;
/// Should show intimacy recommendations
bool get showIntimacyContent => isMarried;
/// Copy with updated fields
UserProfile copyWith({
String? id,
String? name,
RelationshipStatus? relationshipStatus,
FertilityGoal? fertilityGoal,
int? averageCycleLength,
int? averagePeriodLength,
DateTime? lastPeriodStartDate,
bool? notificationsEnabled,
String? devotionalTime,
bool? hasCompletedOnboarding,
DateTime? createdAt,
DateTime? updatedAt,
String? partnerName,
UserRole? role,
bool? isIrregularCycle,
}) {
return UserProfile(
id: id ?? this.id,
name: name ?? this.name,
relationshipStatus: relationshipStatus ?? this.relationshipStatus,
fertilityGoal: fertilityGoal ?? this.fertilityGoal,
averageCycleLength: averageCycleLength ?? this.averageCycleLength,
averagePeriodLength: averagePeriodLength ?? this.averagePeriodLength,
lastPeriodStartDate: lastPeriodStartDate ?? this.lastPeriodStartDate,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
devotionalTime: devotionalTime ?? this.devotionalTime,
hasCompletedOnboarding: hasCompletedOnboarding ?? this.hasCompletedOnboarding,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? DateTime.now(),
partnerName: partnerName ?? this.partnerName,
role: role ?? this.role,
isIrregularCycle: isIrregularCycle ?? this.isIrregularCycle,
);
}
}
@HiveType(typeId: 8)
enum UserRole {
@HiveField(0)
wife,
@HiveField(1)
husband,
}

View File

@@ -0,0 +1,210 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_profile.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserProfileAdapter extends TypeAdapter<UserProfile> {
@override
final int typeId = 2;
@override
UserProfile read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserProfile(
id: fields[0] as String,
name: fields[1] as String,
relationshipStatus: fields[2] as RelationshipStatus,
fertilityGoal: fields[3] as FertilityGoal?,
averageCycleLength: fields[4] as int,
averagePeriodLength: fields[5] as int,
lastPeriodStartDate: fields[6] as DateTime?,
notificationsEnabled: fields[7] as bool,
devotionalTime: fields[8] as String?,
hasCompletedOnboarding: fields[9] as bool,
createdAt: fields[10] as DateTime,
updatedAt: fields[11] as DateTime,
partnerName: fields[12] as String?,
role: fields[14] == null ? UserRole.wife : fields[14] as UserRole,
isIrregularCycle: fields[15] == null ? false : fields[15] as bool,
);
}
@override
void write(BinaryWriter writer, UserProfile obj) {
writer
..writeByte(15)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.relationshipStatus)
..writeByte(3)
..write(obj.fertilityGoal)
..writeByte(4)
..write(obj.averageCycleLength)
..writeByte(5)
..write(obj.averagePeriodLength)
..writeByte(6)
..write(obj.lastPeriodStartDate)
..writeByte(7)
..write(obj.notificationsEnabled)
..writeByte(8)
..write(obj.devotionalTime)
..writeByte(9)
..write(obj.hasCompletedOnboarding)
..writeByte(10)
..write(obj.createdAt)
..writeByte(11)
..write(obj.updatedAt)
..writeByte(12)
..write(obj.partnerName)
..writeByte(14)
..write(obj.role)
..writeByte(15)
..write(obj.isIrregularCycle);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserProfileAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RelationshipStatusAdapter extends TypeAdapter<RelationshipStatus> {
@override
final int typeId = 0;
@override
RelationshipStatus read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return RelationshipStatus.single;
case 1:
return RelationshipStatus.engaged;
case 2:
return RelationshipStatus.married;
default:
return RelationshipStatus.single;
}
}
@override
void write(BinaryWriter writer, RelationshipStatus obj) {
switch (obj) {
case RelationshipStatus.single:
writer.writeByte(0);
break;
case RelationshipStatus.engaged:
writer.writeByte(1);
break;
case RelationshipStatus.married:
writer.writeByte(2);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RelationshipStatusAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class FertilityGoalAdapter extends TypeAdapter<FertilityGoal> {
@override
final int typeId = 1;
@override
FertilityGoal read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return FertilityGoal.tryingToConceive;
case 1:
return FertilityGoal.tryingToAvoid;
case 2:
return FertilityGoal.justTracking;
default:
return FertilityGoal.tryingToConceive;
}
}
@override
void write(BinaryWriter writer, FertilityGoal obj) {
switch (obj) {
case FertilityGoal.tryingToConceive:
writer.writeByte(0);
break;
case FertilityGoal.tryingToAvoid:
writer.writeByte(1);
break;
case FertilityGoal.justTracking:
writer.writeByte(2);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FertilityGoalAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class UserRoleAdapter extends TypeAdapter<UserRole> {
@override
final int typeId = 8;
@override
UserRole read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return UserRole.wife;
case 1:
return UserRole.husband;
default:
return UserRole.wife;
}
}
@override
void write(BinaryWriter writer, UserRole obj) {
switch (obj) {
case UserRole.wife:
writer.writeByte(0);
break;
case UserRole.husband:
writer.writeByte(1);
break;
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserRoleAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/user_profile.dart';
import '../models/cycle_entry.dart';
import '../services/cycle_service.dart';
/// Provider for the user profile
final userProfileProvider = StateNotifierProvider<UserProfileNotifier, UserProfile?>((ref) {
return UserProfileNotifier();
});
/// Notifier for the user profile
class UserProfileNotifier extends StateNotifier<UserProfile?> {
UserProfileNotifier() : super(null) {
_loadProfile();
}
void _loadProfile() {
final box = Hive.box<UserProfile>('user_profile');
state = box.get('current_user');
}
Future<void> updateProfile(UserProfile profile) async {
final box = Hive.box<UserProfile>('user_profile');
await box.put('current_user', profile);
state = profile;
}
Future<void> clearProfile() async {
final box = Hive.box<UserProfile>('user_profile');
await box.clear();
state = null;
}
}
/// Provider for cycle entries
final cycleEntriesProvider = StateNotifierProvider<CycleEntriesNotifier, List<CycleEntry>>((ref) {
return CycleEntriesNotifier();
});
/// Notifier for cycle entries
class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
CycleEntriesNotifier() : super([]) {
_loadEntries();
}
void _loadEntries() {
final box = Hive.box<CycleEntry>('cycle_entries');
state = box.values.toList()..sort((a, b) => b.date.compareTo(a.date));
}
Future<void> addEntry(CycleEntry entry) async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.put(entry.id, entry);
_loadEntries();
}
Future<void> deleteEntry(String id) async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.delete(id);
_loadEntries();
}
Future<void> clearEntries() async {
final box = Hive.box<CycleEntry>('cycle_entries');
await box.clear();
state = [];
}
}
/// Computed provider for current cycle info
final currentCycleInfoProvider = Provider((ref) {
final user = ref.watch(userProfileProvider);
return CycleService.calculateCycleInfo(user);
});

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,
),
),
],
),
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,65 @@
import '../models/user_profile.dart';
import '../models/cycle_entry.dart';
class CycleService {
/// Calculates the current cycle information based on user profile
static Map<String, dynamic> calculateCycleInfo(UserProfile? user) {
if (user?.lastPeriodStartDate == null) {
return {
'phase': CyclePhase.follicular,
'dayOfCycle': 1,
'daysUntilPeriod': user?.averageCycleLength ?? 28,
'isPeriodExpected': false,
};
}
final lastPeriod = user!.lastPeriodStartDate!;
final cycleLength = user.averageCycleLength;
final now = DateTime.now();
// Normalize dates to midnight for accurate day counting
final startOfToday = DateTime(now.year, now.month, now.day);
final startOfLastPeriod = DateTime(lastPeriod.year, lastPeriod.month, lastPeriod.day);
final daysSinceLastPeriod = startOfToday.difference(startOfLastPeriod).inDays + 1;
// Handle cases where last period was long ago (more than one cycle)
final dayOfCycle = ((daysSinceLastPeriod - 1) % cycleLength) + 1;
final daysUntilPeriod = cycleLength - dayOfCycle;
CyclePhase phase;
if (dayOfCycle <= 5) {
phase = CyclePhase.menstrual;
} else if (dayOfCycle <= 13) {
phase = CyclePhase.follicular;
} else if (dayOfCycle <= 16) {
phase = CyclePhase.ovulation;
} else {
phase = CyclePhase.luteal;
}
return {
'phase': phase,
'dayOfCycle': dayOfCycle,
'daysUntilPeriod': daysUntilPeriod,
'isPeriodExpected': daysUntilPeriod <= 0 || dayOfCycle <= 5,
};
}
/// Format cycle day for display
static String getDayOfCycleDisplay(int day) => 'Day $day';
/// Get phase description
static String getPhaseDescription(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return 'Your body is resting and clearing. Be gentle with yourself.';
case CyclePhase.follicular:
return 'Energy and optimism are rising. A great time for new projects.';
case CyclePhase.ovulation:
return 'You are at your peak energy and fertility.';
case CyclePhase.luteal:
return 'Progesterone is rising. You may feel more introverted or sensitive.';
}
}
}

307
lib/theme/app_theme.dart Normal file
View File

@@ -0,0 +1,307 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// App color palette for wife's experience
class AppColors {
// Primary Colors (Wife's App)
static const Color blushPink = Color(0xFFF8E1E7);
static const Color rose = Color(0xFFE8A0B0);
static const Color sageGreen = Color(0xFFA8C5A8);
static const Color lavender = Color(0xFFD4C4E8);
static const Color cream = Color(0xFFFDF8F5);
static const Color softGold = Color(0xFFD4A574);
// Text Colors
static const Color charcoal = Color(0xFF3D3D3D);
static const Color warmGray = Color(0xFF7A7A7A);
static const Color lightGray = Color(0xFFB8B8B8);
// Husband's App Colors
static const Color navyBlue = Color(0xFF2C3E50);
static const Color steelBlue = Color(0xFF5D7B93);
static const Color warmCream = Color(0xFFF5F0E8);
static const Color gold = Color(0xFFC9A961);
static const Color softCoral = Color(0xFFE8B4A8);
// Phase Colors
static const Color menstrualPhase = Color(0xFFE88A9E);
static const Color follicularPhase = Color(0xFF8BC5A3);
static const Color ovulationPhase = Color(0xFFB8A5D4);
static const Color lutealPhase = Color(0xFF8BA5C5);
// Semantic Colors
static const Color success = Color(0xFF7AB98A);
static const Color warning = Color(0xFFE8C567);
static const Color error = Color(0xFFE87B7B);
static const Color info = Color(0xFF7BB8E8);
}
/// App theme configuration
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// Color Scheme
colorScheme: const ColorScheme.light(
primary: AppColors.sageGreen,
secondary: AppColors.rose,
tertiary: AppColors.lavender,
surface: AppColors.cream,
error: AppColors.error,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: AppColors.charcoal,
),
// Scaffold
scaffoldBackgroundColor: AppColors.cream,
// AppBar
appBarTheme: AppBarTheme(
backgroundColor: AppColors.cream,
foregroundColor: AppColors.charcoal,
elevation: 0,
centerTitle: true,
titleTextStyle: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
// Text Theme
textTheme: TextTheme(
displayLarge: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
displayMedium: GoogleFonts.outfit(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
headlineLarge: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
headlineMedium: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
titleLarge: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
titleMedium: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
bodyLarge: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.charcoal,
),
bodyMedium: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.charcoal,
),
bodySmall: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.warmGray,
),
labelLarge: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.charcoal,
),
),
// Card Theme
cardTheme: CardTheme(
color: Colors.white,
elevation: 2,
shadowColor: AppColors.charcoal.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
// Button Themes
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.sageGreen,
side: const BorderSide(color: AppColors.sageGreen, width: 1.5),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.rose,
textStyle: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
// Input Decoration
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColors.lightGray.withOpacity(0.5)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColors.lightGray.withOpacity(0.5)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.sageGreen, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
hintStyle: GoogleFonts.outfit(
color: AppColors.lightGray,
fontSize: 14,
),
),
// Bottom Navigation
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Colors.white,
selectedItemColor: AppColors.sageGreen,
unselectedItemColor: AppColors.warmGray,
type: BottomNavigationBarType.fixed,
elevation: 8,
selectedLabelStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
// Floating Action Button
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppColors.sageGreen,
foregroundColor: Colors.white,
elevation: 4,
),
// Slider Theme
sliderTheme: SliderThemeData(
activeTrackColor: AppColors.sageGreen,
inactiveTrackColor: AppColors.lightGray.withOpacity(0.3),
thumbColor: AppColors.sageGreen,
overlayColor: AppColors.sageGreen.withOpacity(0.2),
trackHeight: 4,
),
// Divider
dividerTheme: DividerThemeData(
color: AppColors.lightGray.withOpacity(0.3),
thickness: 1,
space: 24,
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: const ColorScheme.dark(
primary: AppColors.sageGreen,
secondary: AppColors.rose,
tertiary: AppColors.lavender,
surface: Color(0xFF1E1E1E),
error: AppColors.error,
),
scaffoldBackgroundColor: const Color(0xFF121212),
textTheme: TextTheme(
displayLarge: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Colors.white,
),
bodyLarge: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white,
),
bodyMedium: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.white70,
),
),
cardTheme: CardTheme(
color: const Color(0xFF1E1E1E),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
}
/// Scripture text style
TextStyle scriptureStyle(BuildContext context, {double? fontSize}) {
return GoogleFonts.lora(
fontSize: fontSize ?? 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
);
}
/// Scripture reference style
TextStyle scriptureRefStyle(BuildContext context) {
return GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
);
}

195
lib/widgets/cycle_ring.dart Normal file
View File

@@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math;
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
class CycleRing extends StatelessWidget {
final int dayOfCycle;
final int totalDays;
final CyclePhase phase;
const CycleRing({
super.key,
required this.dayOfCycle,
required this.totalDays,
required this.phase,
});
@override
Widget build(BuildContext context) {
final progress = dayOfCycle / totalDays;
final daysUntilNextPeriod = totalDays - dayOfCycle;
return Container(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
// Background ring
CustomPaint(
size: const Size(220, 220),
painter: _CycleRingPainter(
progress: progress,
phase: phase,
),
),
// Center content
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Day $dayOfCycle',
style: GoogleFonts.outfit(
fontSize: 32,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(phase).withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
phase.emoji,
style: const TextStyle(fontSize: 14),
),
const SizedBox(width: 6),
Text(
phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _getPhaseColor(phase),
),
),
],
),
),
const SizedBox(height: 8),
Text(
daysUntilNextPeriod > 0
? '$daysUntilNextPeriod days until period'
: 'Period expected',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
),
),
],
),
],
),
);
}
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;
}
}
}
class _CycleRingPainter extends CustomPainter {
final double progress;
final CyclePhase phase;
_CycleRingPainter({required this.progress, required this.phase});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 15;
const strokeWidth = 12.0;
// Background arc
final bgPaint = Paint()
..color = AppColors.lightGray.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bgPaint);
// Progress arc
final progressPaint = Paint()
..shader = SweepGradient(
startAngle: -math.pi / 2,
endAngle: math.pi * 1.5,
colors: _getGradientColors(phase),
stops: const [0.0, 0.5, 1.0],
).createShader(Rect.fromCircle(center: center, radius: radius))
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2,
2 * math.pi * progress,
false,
progressPaint,
);
// Dot at current position
final dotAngle = -math.pi / 2 + 2 * math.pi * progress;
final dotX = center.dx + radius * math.cos(dotAngle);
final dotY = center.dy + radius * math.sin(dotAngle);
final dotPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
final dotBorderPaint = Paint()
..color = _getPhaseColor(phase)
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawCircle(Offset(dotX, dotY), 8, dotPaint);
canvas.drawCircle(Offset(dotX, dotY), 8, dotBorderPaint);
}
List<Color> _getGradientColors(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return [AppColors.rose, AppColors.menstrualPhase, AppColors.blushPink];
case CyclePhase.follicular:
return [AppColors.sageGreen, AppColors.follicularPhase, AppColors.sageGreen.withOpacity(0.7)];
case CyclePhase.ovulation:
return [AppColors.lavender, AppColors.ovulationPhase, AppColors.rose];
case CyclePhase.luteal:
return [AppColors.lutealPhase, AppColors.lavender, AppColors.blushPink];
}
}
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;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../screens/log/log_screen.dart';
class QuickLogButtons extends StatelessWidget {
const QuickLogButtons({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
_buildQuickButton(
icon: Icons.water_drop_outlined,
label: 'Period',
color: AppColors.menstrualPhase,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.emoji_emotions_outlined,
label: 'Mood',
color: AppColors.softGold,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.flash_on_outlined,
label: 'Energy',
color: AppColors.follicularPhase,
onTap: () => _navigateToLog(context),
),
const SizedBox(width: 12),
_buildQuickButton(
icon: Icons.healing_outlined,
label: 'Symptoms',
color: AppColors.lavender,
onTap: () => _navigateToLog(context),
),
],
);
}
void _navigateToLog(BuildContext context) {
// Navigate to the Log tab (index 2) of HomeScreen if possible,
// but since we are inside a tab, we can't easily switch the parent tab index without context. Using a provider or callback would be best.
// For now, let's push the LogScreen as a new route for "Quick Log" feel.
// Ideally we would switch the BottomNavBar index.
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(0),
child: SizedBox.shrink()
),
body: LogScreen()
)),
);
}
Widget _buildQuickButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 6),
Text(
label,
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.w500,
color: color,
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
class ScriptureCard extends StatelessWidget {
final String verse;
final String reference;
final CyclePhase phase;
const ScriptureCard({
super.key,
required this.verse,
required this.reference,
required this.phase,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _getGradientColors(phase),
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: _getPhaseColor(phase).withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Scripture icon
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.menu_book_outlined,
size: 18,
color: AppColors.charcoal.withOpacity(0.8),
),
),
const SizedBox(width: 8),
Text(
'Today\'s Verse',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.charcoal.withOpacity(0.7),
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 16),
// Verse
Text(
'"$verse"',
style: GoogleFonts.lora(
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.charcoal,
height: 1.6,
),
),
const SizedBox(height: 12),
// Reference
Text(
'$reference',
style: GoogleFonts.outfit(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
],
),
);
}
List<Color> _getGradientColors(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return [
AppColors.blushPink.withOpacity(0.6),
AppColors.cream,
];
case CyclePhase.follicular:
return [
AppColors.sageGreen.withOpacity(0.3),
AppColors.cream,
];
case CyclePhase.ovulation:
return [
AppColors.lavender.withOpacity(0.5),
AppColors.cream,
];
case CyclePhase.luteal:
return [
AppColors.lutealPhase.withOpacity(0.3),
AppColors.cream,
];
}
}
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;
}
}
}

95
lib/widgets/tip_card.dart Normal file
View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
class TipCard extends StatelessWidget {
final CyclePhase phase;
final bool isMarried;
const TipCard({
super.key,
required this.phase,
required this.isMarried,
});
@override
Widget build(BuildContext context) {
final tip = _getTipForPhase(phase, isMarried);
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: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.sageGreen.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.lightbulb_outline,
color: AppColors.sageGreen,
size: 22,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Today\'s Tip',
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
),
),
const SizedBox(height: 4),
Text(
tip,
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
height: 1.4,
),
),
],
),
),
],
),
);
}
String _getTipForPhase(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
return 'This is a time for rest. Honor your body with extra sleep, warm drinks, and gentle movement. God designed your body with wisdom.';
case CyclePhase.follicular:
return 'Your energy is rising! This is a great time to start new projects, exercise more intensely, and spend time in community.';
case CyclePhase.ovulation:
if (isMarried) {
return 'This is your most fertile window. You may feel more social and energetic. Prioritize connection with your spouse.';
}
return 'You may feel more social and confident during this phase. It\'s a great time for important conversations and presentations.';
case CyclePhase.luteal:
return 'As you enter the luteal phase, focus on nourishing foods, adequate sleep, and stress management. Be gentle with yourself.';
}
}
}