258 lines
8.1 KiB
Dart
258 lines
8.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:relationship_saver/core/config/app_theme.dart';
|
|
import 'package:relationship_saver/features/local/local_models.dart';
|
|
import 'package:relationship_saver/features/local/local_repository.dart';
|
|
import 'package:relationship_saver/features/shared/frosted_card.dart';
|
|
|
|
class DashboardView extends ConsumerWidget {
|
|
const DashboardView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final AsyncValue<LocalDataState> localData = ref.watch(
|
|
localRepositoryProvider,
|
|
);
|
|
|
|
return localData.when(
|
|
data: (LocalDataState data) {
|
|
final DashboardSummary summary = data.summary();
|
|
final List<DashboardTask> tasks = data.tasks;
|
|
|
|
return LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
final bool compact = constraints.maxWidth < 720;
|
|
return SingleChildScrollView(
|
|
padding: EdgeInsets.fromLTRB(
|
|
compact ? 16 : 28,
|
|
compact ? 16 : 24,
|
|
compact ? 16 : 28,
|
|
compact ? 24 : 36,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Text(
|
|
'Today',
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Focus on consistency over intensity. Small moments compound.',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
const SizedBox(height: 22),
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 12,
|
|
children: <Widget>[
|
|
_StatCard(
|
|
label: 'Active People',
|
|
value: '${summary.activePeople}',
|
|
accent: const Color(0xFF16B8CA),
|
|
compact: compact,
|
|
),
|
|
_StatCard(
|
|
label: 'Upcoming Plans',
|
|
value: '${summary.upcomingPlans}',
|
|
accent: const Color(0xFF2B7FFF),
|
|
compact: compact,
|
|
),
|
|
_StatCard(
|
|
label: 'Pending Ideas',
|
|
value: '${summary.pendingIdeas}',
|
|
accent: const Color(0xFFFFA447),
|
|
compact: compact,
|
|
),
|
|
_StatCard(
|
|
label: 'Weekly Rhythm',
|
|
value: '${summary.weeklyConsistency}%',
|
|
accent: const Color(0xFF30B66A),
|
|
compact: compact,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
FrostedCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Text(
|
|
'Next Moves',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 14),
|
|
...tasks.map(
|
|
(DashboardTask task) => _TaskRow(
|
|
task: task,
|
|
compact: compact,
|
|
onToggleDone: () {
|
|
ref
|
|
.read(localRepositoryProvider.notifier)
|
|
.toggleTaskDone(task.id);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (Object error, StackTrace stackTrace) {
|
|
return Center(
|
|
child: Text(
|
|
'Could not load local data.',
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _StatCard extends StatelessWidget {
|
|
const _StatCard({
|
|
required this.label,
|
|
required this.value,
|
|
required this.accent,
|
|
required this.compact,
|
|
});
|
|
|
|
final String label;
|
|
final String value;
|
|
final Color accent;
|
|
final bool compact;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FrostedCard(
|
|
padding: const EdgeInsets.all(16),
|
|
child: SizedBox(
|
|
width: compact ? double.infinity : 188,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Container(
|
|
height: 8,
|
|
width: 40,
|
|
decoration: BoxDecoration(
|
|
color: accent,
|
|
borderRadius: BorderRadius.circular(99),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
value,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
label,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TaskRow extends StatelessWidget {
|
|
const _TaskRow({
|
|
required this.task,
|
|
required this.onToggleDone,
|
|
required this.compact,
|
|
});
|
|
|
|
final DashboardTask task;
|
|
final VoidCallback onToggleDone;
|
|
final bool compact;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF5FAFB),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
InkWell(
|
|
onTap: onToggleDone,
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Icon(
|
|
task.done
|
|
? Icons.check_circle_rounded
|
|
: Icons.radio_button_unchecked_rounded,
|
|
color: task.done
|
|
? const Color(0xFF1D9C66)
|
|
: const Color(0xFF1AB6C8),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Text(
|
|
task.title,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
decoration: task.done ? TextDecoration.lineThrough : null,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
task.description,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (!compact) ...<Widget>[
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
_shortDate(task.when),
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _shortDate(DateTime value) {
|
|
const List<String> weekdays = <String>[
|
|
'Mon',
|
|
'Tue',
|
|
'Wed',
|
|
'Thu',
|
|
'Fri',
|
|
'Sat',
|
|
'Sun',
|
|
];
|
|
final String weekday = weekdays[value.weekday - 1];
|
|
final String day = value.day.toString().padLeft(2, '0');
|
|
return '$weekday $day';
|
|
}
|