Your commit message here

This commit is contained in:
2025-12-19 22:47:27 -06:00
parent 5d746d694e
commit 464692ce56
21 changed files with 3018 additions and 0 deletions

View File

@@ -4,7 +4,11 @@ import 'dart:math' as math;
import '../theme/app_theme.dart';
import '../models/cycle_entry.dart';
<<<<<<< HEAD
class CycleRing extends StatelessWidget {
=======
class CycleRing extends StatefulWidget {
>>>>>>> 6742220 (Your commit message here)
final int dayOfCycle;
final int totalDays;
final CyclePhase phase;
@@ -17,6 +21,7 @@ class CycleRing extends StatelessWidget {
});
@override
<<<<<<< HEAD
Widget build(BuildContext context) {
final progress = dayOfCycle / totalDays;
final daysUntilNextPeriod = totalDays - dayOfCycle;
@@ -82,12 +87,128 @@ class CycleRing extends StatelessWidget {
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray,
=======
State<CycleRing> createState() => _CycleRingState();
}
class _CycleRingState extends State<CycleRing> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final targetProgress = widget.dayOfCycle / widget.totalDays;
final daysUntilNextPeriod = widget.totalDays - widget.dayOfCycle;
final isDark = Theme.of(context).brightness == Brightness.dark;
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final currentProgress = targetProgress * _animation.value;
return SizedBox(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
// Background ring
CustomPaint(
size: const Size(220, 220),
painter: _CycleRingPainter(
progress: currentProgress,
phase: widget.phase,
isDark: isDark,
),
),
// Center content with scale and fade animation
Transform.scale(
scale: 0.8 + (0.2 * _animation.value),
child: Opacity(
opacity: _animation.value,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Day ${widget.dayOfCycle}',
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getPhaseColor(widget.phase).withOpacity(isDark ? 0.3 : 0.2),
borderRadius: BorderRadius.circular(20),
border: isDark ? Border.all(color: _getPhaseColor(widget.phase).withOpacity(0.5)) : null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.phase.emoji,
style: const TextStyle(fontSize: 14),
),
const SizedBox(width: 6),
Text(
widget.phase.label,
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isDark ? Colors.white : _getPhaseColor(widget.phase),
),
),
],
),
),
const SizedBox(height: 8),
Text(
daysUntilNextPeriod > 0
? '$daysUntilNextPeriod days until period'
: 'Period expected',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
>>>>>>> 6742220 (Your commit message here)
),
),
],
),
<<<<<<< HEAD
],
),
=======
);
},
>>>>>>> 6742220 (Your commit message here)
);
}
@@ -108,8 +229,18 @@ class CycleRing extends StatelessWidget {
class _CycleRingPainter extends CustomPainter {
final double progress;
final CyclePhase phase;
<<<<<<< HEAD
_CycleRingPainter({required this.progress, required this.phase});
=======
final bool isDark;
_CycleRingPainter({
required this.progress,
required this.phase,
required this.isDark,
});
>>>>>>> 6742220 (Your commit message here)
@override
void paint(Canvas canvas, Size size) {
@@ -119,7 +250,11 @@ class _CycleRingPainter extends CustomPainter {
// Background arc
final bgPaint = Paint()
<<<<<<< HEAD
..color = AppColors.lightGray.withOpacity(0.2)
=======
..color = (isDark ? Colors.white : AppColors.lightGray).withOpacity(isDark ? 0.05 : 0.1)
>>>>>>> 6742220 (Your commit message here)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
@@ -152,7 +287,11 @@ class _CycleRingPainter extends CustomPainter {
final dotY = center.dy + radius * math.sin(dotAngle);
final dotPaint = Paint()
<<<<<<< HEAD
..color = Colors.white
=======
..color = isDark ? const Color(0xFF1E1E1E) : Colors.white
>>>>>>> 6742220 (Your commit message here)
..style = PaintingStyle.fill;
final dotBorderPaint = Paint()

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
<<<<<<< HEAD
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../screens/log/log_screen.dart';
@@ -36,11 +37,56 @@ class QuickLogButtons extends StatelessWidget {
label: 'Symptoms',
color: AppColors.lavender,
onTap: () => _navigateToLog(context),
=======
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../theme/app_theme.dart';
import '../providers/navigation_provider.dart';
class QuickLogButtons extends ConsumerWidget {
const QuickLogButtons({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Row(
children: [
_buildQuickButton(
context,
icon: Icons.water_drop_outlined,
label: 'Period',
color: AppColors.menstrualPhase,
onTap: () => _navigateToLog(ref),
),
const SizedBox(width: 12),
_buildQuickButton(
context,
icon: Icons.emoji_emotions_outlined,
label: 'Mood',
color: AppColors.softGold,
onTap: () => _navigateToLog(ref),
),
const SizedBox(width: 12),
_buildQuickButton(
context,
icon: Icons.flash_on_outlined,
label: 'Energy',
color: AppColors.follicularPhase,
onTap: () => _navigateToLog(ref),
),
const SizedBox(width: 12),
_buildQuickButton(
context,
icon: Icons.healing_outlined,
label: 'Symptoms',
color: AppColors.lavender,
onTap: () => _navigateToLog(ref),
>>>>>>> 6742220 (Your commit message here)
),
],
);
}
<<<<<<< HEAD
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.
@@ -59,11 +105,21 @@ class QuickLogButtons extends StatelessWidget {
}
Widget _buildQuickButton({
=======
void _navigateToLog(WidgetRef ref) {
// Navigate to the Log tab (index 2)
ref.read(navigationProvider.notifier).setIndex(2);
}
Widget _buildQuickButton(
BuildContext context, {
>>>>>>> 6742220 (Your commit message here)
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
<<<<<<< HEAD
return Expanded(
child: GestureDetector(
onTap: onTap,
@@ -87,6 +143,38 @@ class QuickLogButtons extends StatelessWidget {
),
),
],
=======
final isDark = Theme.of(context).brightness == Brightness.dark;
return Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: color.withOpacity(isDark ? 0.2 : 0.15),
borderRadius: BorderRadius.circular(12),
border: isDark ? Border.all(color: color.withOpacity(0.3)) : null,
),
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.w600,
color: isDark ? Colors.white.withOpacity(0.9) : color,
),
),
],
),
>>>>>>> 6742220 (Your commit message here)
),
),
),

View File

@@ -6,35 +6,66 @@ import '../models/cycle_entry.dart';
class ScriptureCard extends StatelessWidget {
final String verse;
final String reference;
<<<<<<< HEAD
final CyclePhase phase;
=======
final String? translation;
final CyclePhase phase;
final VoidCallback? onTranslationTap;
>>>>>>> 6742220 (Your commit message here)
const ScriptureCard({
super.key,
required this.verse,
required this.reference,
<<<<<<< HEAD
required this.phase,
=======
this.translation,
required this.phase,
this.onTranslationTap,
>>>>>>> 6742220 (Your commit message here)
});
@override
Widget build(BuildContext context) {
<<<<<<< HEAD
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _getGradientColors(phase),
=======
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _getGradientColors(context, phase),
>>>>>>> 6742220 (Your commit message here)
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
<<<<<<< HEAD
boxShadow: [
BoxShadow(
color: _getPhaseColor(phase).withOpacity(0.2),
=======
border: Border.all(color: isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.05)),
boxShadow: [
BoxShadow(
color: _getPhaseColor(phase).withOpacity(isDark ? 0.05 : 0.15),
>>>>>>> 6742220 (Your commit message here)
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
<<<<<<< HEAD
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -90,10 +121,103 @@ class ScriptureCard extends StatelessWidget {
),
),
],
=======
child: Material(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Scripture icon
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.menu_book_outlined,
size: 18,
color: isDark ? Colors.white70 : AppColors.charcoal.withOpacity(0.8),
),
),
const SizedBox(width: 8),
Text(
'Today\'s Verse',
style: theme.textTheme.labelLarge?.copyWith(
fontSize: 12,
color: isDark ? Colors.white60 : AppColors.charcoal.withOpacity(0.7),
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 16),
// Verse
Text(
'"$verse"',
style: scriptureStyle(context, fontSize: 17),
),
const SizedBox(height: 12),
// Reference & Translation
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$reference',
style: scriptureRefStyle(context).copyWith(fontSize: 13, fontWeight: FontWeight.w600),
),
if (translation != null)
InkWell(
onTap: onTranslationTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: (isDark ? Colors.white : Colors.black).withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: (isDark ? Colors.white : Colors.black).withOpacity(0.1),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
translation!,
style: scriptureRefStyle(context).copyWith(
fontSize: 10,
fontWeight: FontWeight.w800,
letterSpacing: 0.8,
),
),
const SizedBox(width: 4),
Icon(
Icons.swap_horiz,
size: 14,
color: isDark ? Colors.white38 : AppColors.warmGray,
),
],
),
),
),
],
),
],
),
),
>>>>>>> 6742220 (Your commit message here)
),
);
}
<<<<<<< HEAD
List<Color> _getGradientColors(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
@@ -115,6 +239,32 @@ class ScriptureCard extends StatelessWidget {
return [
AppColors.lutealPhase.withOpacity(0.3),
AppColors.cream,
=======
List<Color> _getGradientColors(BuildContext context, CyclePhase phase) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final baseColor = isDark ? const Color(0xFF1E1E1E) : AppColors.cream;
switch (phase) {
case CyclePhase.menstrual:
return [
AppColors.menstrualPhase.withOpacity(isDark ? 0.15 : 0.6),
baseColor,
];
case CyclePhase.follicular:
return [
AppColors.follicularPhase.withOpacity(isDark ? 0.15 : 0.3),
baseColor,
];
case CyclePhase.ovulation:
return [
AppColors.ovulationPhase.withOpacity(isDark ? 0.15 : 0.5),
baseColor,
];
case CyclePhase.luteal:
return [
AppColors.lutealPhase.withOpacity(isDark ? 0.15 : 0.3),
baseColor,
>>>>>>> 6742220 (Your commit message here)
];
}
}

View File

@@ -15,17 +15,31 @@ class TipCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
<<<<<<< HEAD
=======
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
>>>>>>> 6742220 (Your commit message here)
final tip = _getTipForPhase(phase, isMarried);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
<<<<<<< HEAD
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.charcoal.withOpacity(0.05),
=======
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
border: isDark ? Border.all(color: Colors.white.withOpacity(0.05)) : null,
boxShadow: [
BoxShadow(
color: (isDark ? Colors.black : AppColors.charcoal).withOpacity(0.05),
>>>>>>> 6742220 (Your commit message here)
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -38,7 +52,11 @@ class TipCard extends StatelessWidget {
width: 40,
height: 40,
decoration: BoxDecoration(
<<<<<<< HEAD
color: AppColors.sageGreen.withOpacity(0.15),
=======
color: AppColors.sageGreen.withOpacity(isDark ? 0.2 : 0.15),
>>>>>>> 6742220 (Your commit message here)
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
@@ -54,21 +72,57 @@ class TipCard extends StatelessWidget {
children: [
Text(
'Today\'s Tip',
<<<<<<< HEAD
style: GoogleFonts.outfit(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.charcoal,
=======
style: theme.textTheme.titleMedium?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
>>>>>>> 6742220 (Your commit message here)
),
),
const SizedBox(height: 4),
Text(
tip,
<<<<<<< HEAD
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.warmGray,
height: 1.4,
),
),
=======
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 13,
height: 1.4,
),
),
const SizedBox(height: 12),
InkWell(
onTap: () => _showDetailedInsights(context),
child: Row(
children: [
Text(
'Learn More',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.sageGreen,
),
),
const SizedBox(width: 4),
const Icon(
Icons.arrow_forward_ios,
size: 10,
color: AppColors.sageGreen,
),
],
),
),
>>>>>>> 6742220 (Your commit message here)
],
),
),
@@ -77,6 +131,7 @@ class TipCard extends StatelessWidget {
);
}
<<<<<<< HEAD
String _getTipForPhase(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
@@ -90,6 +145,123 @@ class TipCard extends StatelessWidget {
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.';
=======
void _showDetailedInsights(BuildContext context) {
final details = _getDetailsForPhase(phase);
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Column(
children: [
Text(
'Phase Insight: ${phase.label}',
textAlign: TextAlign.center,
style: GoogleFonts.outfit(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Text(
phase.emoji,
style: const TextStyle(fontSize: 32),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailSection('Nutrition', details['nutrition']!, Icons.restaurant),
const SizedBox(height: 16),
_buildDetailSection('Movement', details['movement']!, Icons.fitness_center),
const SizedBox(height: 16),
Text(
'Note: While these are general trends, your body is unique. Always listen to your own energy levels.',
style: GoogleFonts.outfit(
fontSize: 11,
fontStyle: FontStyle.italic,
color: AppColors.warmGray,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Got it', style: GoogleFonts.outfit(color: AppColors.sageGreen)),
),
],
),
);
}
Widget _buildDetailSection(String title, String content, IconData icon) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 16, color: AppColors.sageGreen),
const SizedBox(width: 6),
Text(
title,
style: GoogleFonts.outfit(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
const SizedBox(height: 6),
Text(
content,
style: GoogleFonts.outfit(
fontSize: 13,
color: AppColors.charcoal,
),
),
],
);
}
String _getTipForPhase(CyclePhase phase, bool isMarried) {
switch (phase) {
case CyclePhase.menstrual:
return 'Focus on iron-rich foods and gentle rest. Your body is working hard; honor it with hydration and Vitamin C.';
case CyclePhase.follicular:
return 'Energy is rising! Support your cycle with cruciferous vegetables and lean protein as you transition to more active movement.';
case CyclePhase.ovulation:
if (isMarried) {
return 'Peak energy and connection. Focus on healthy fats for hormonal support and prioritize quality time with your spouse.';
}
return 'Peak confidence and social energy. Support your peak energy with complex carbs and social engagement.';
case CyclePhase.luteal:
return 'Nourish your nervous system with magnesium-rich foods and steady mobility. Transition to restorative self-care.';
}
}
Map<String, String> _getDetailsForPhase(CyclePhase phase) {
switch (phase) {
case CyclePhase.menstrual:
return {
'nutrition': 'Incorporate leafy greens, red meat or lentils for iron. Pair with citrus for better absorption.',
'movement': 'Gentle walking, restorative yoga, or just deep breathing. Avoid high-intensity stress.',
};
case CyclePhase.follicular:
return {
'nutrition': 'Broccoli, cauliflower, and fermented foods help balance rising estrogen. Focus on lean proteins.',
'movement': 'Strength training and steady cardio. Your body is primed for building and renewal.',
};
case CyclePhase.ovulation:
return {
'nutrition': 'Avocados, nuts, and seeds provide healthy fats for peak hormonal health. Keep hydration high.',
'movement': 'Highest intensity workouts, HIIT, or group sports. You have peak stamina and strength right now.',
};
case CyclePhase.luteal:
return {
'nutrition': 'Dark chocolate (70%+), pumpkin seeds, and bananas for magnesium to help with cramps.',
'movement': 'Pilates, steady-state swimming, or hiking. Focus on persistence rather than peak intensity.',
};
>>>>>>> 6742220 (Your commit message here)
}
}
}