import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../core/constants.dart'; import '../data/models/note.dart'; import '../data/models/project.dart'; import '../data/models/task.dart'; import '../providers/tasks_provider.dart'; // ── Note card ──────────────────────────────────────────────────────────────── class NoteLibraryCard extends StatelessWidget { final Note note; const NoteLibraryCard({super.key, required this.note}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: InkWell( onTap: () => context .push(Routes.noteDetail.replaceFirst(':id', '${note.id}')), borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.article_outlined, size: 15, color: theme.colorScheme.onSurfaceVariant), const SizedBox(width: 6), Expanded( child: Text( note.title.isNotEmpty ? note.title : 'Untitled', style: theme.textTheme.titleSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Text( _relativeTime(note.updatedAt), style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant), ), ], ), if (note.body.isNotEmpty) ...[ const SizedBox(height: 4), Text( note.body.replaceAll('\n', ' '), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], if (note.tags.isNotEmpty) ...[ const SizedBox(height: 6), Wrap( spacing: 4, runSpacing: 2, children: note.tags .take(4) .map((t) => Chip( label: Text(t), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, )) .toList(), ), ], ], ), ), ), ); } } // ── Task card ──────────────────────────────────────────────────────────────── class TaskLibraryCard extends ConsumerWidget { final Task task; const TaskLibraryCard({super.key, required this.task}); Color _priorityColor(BuildContext context) { return switch (task.priority) { TaskPriority.high => const Color(0xFFEF4444), TaskPriority.medium => const Color(0xFFF59E0B), _ => Theme.of(context).colorScheme.onSurfaceVariant, }; } IconData get _statusIcon => switch (task.status) { TaskStatus.done => Icons.check_circle, TaskStatus.inProgress => Icons.timelapse, _ => Icons.radio_button_unchecked, }; Color _statusColor(BuildContext context) { final cs = Theme.of(context).colorScheme; return switch (task.status) { TaskStatus.done => const Color(0xFF22C55E), TaskStatus.inProgress => cs.primary, _ => cs.onSurfaceVariant, }; } TaskStatus get _nextStatus => switch (task.status) { TaskStatus.todo => TaskStatus.inProgress, TaskStatus.inProgress => TaskStatus.done, _ => TaskStatus.todo, }; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: InkWell( onTap: () => context .push(Routes.taskEdit.replaceFirst(':id', '${task.id}')), borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.fromLTRB(8, 10, 14, 10), child: Row( children: [ // Status cycle button IconButton( icon: Icon(_statusIcon, color: _statusColor(context)), onPressed: () => ref .read(tasksProvider.notifier) .updateTask(task.id, {'status': _nextStatus.value}), tooltip: 'Cycle status', visualDensity: VisualDensity.compact, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.title.isNotEmpty ? task.title : 'Untitled', style: theme.textTheme.titleSmall?.copyWith( decoration: task.status == TaskStatus.done ? TextDecoration.lineThrough : null, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (task.dueDate != null) ...[ const SizedBox(height: 2), Text( 'Due ${_formatDate(task.dueDate!)}', style: theme.textTheme.labelSmall?.copyWith( color: task.dueDate!.isBefore(DateTime.now()) && task.status != TaskStatus.done ? const Color(0xFFEF4444) : theme.colorScheme.onSurfaceVariant, ), ), ], ], ), ), if (task.priority != TaskPriority.none && task.priority != TaskPriority.low) Container( width: 8, height: 8, decoration: BoxDecoration( color: _priorityColor(context), shape: BoxShape.circle, ), ), ], ), ), ), ); } } // ── Project card ───────────────────────────────────────────────────────────── class ProjectLibraryCard extends StatelessWidget { final Project project; const ProjectLibraryCard({super.key, required this.project}); Color _parseColor(String? hex) { if (hex == null || hex.isEmpty) return const Color(0xFF6366F1); try { return Color(int.parse(hex.replaceFirst('#', '0xFF'))); } catch (_) { return const Color(0xFF6366F1); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final color = _parseColor(project.color); return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), clipBehavior: Clip.antiAlias, child: InkWell( onTap: () => context .push('/projects/${project.id}/tasks'), borderRadius: BorderRadius.circular(14), child: Row( children: [ // Colour strip Container(width: 6, height: 64, color: color), const SizedBox(width: 12), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( project.title, style: theme.textTheme.titleSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), if (project.description?.isNotEmpty == true) ...[ const SizedBox(height: 2), Text( project.description!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), ), const SizedBox(width: 12), ], ), ), ); } } // ── Helpers ─────────────────────────────────────────────────────────────────── String _relativeTime(DateTime dt) { final diff = DateTime.now().difference(dt); if (diff.inMinutes < 1) return 'just now'; if (diff.inHours < 1) return '${diff.inMinutes}m ago'; if (diff.inDays < 1) return '${diff.inHours}h ago'; if (diff.inDays < 7) return '${diff.inDays}d ago'; return _formatDate(dt); } String _formatDate(DateTime dt) { const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; return '${months[dt.month - 1]} ${dt.day}'; }