mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-04 12:02:56 +00:00
Added core data models, repositories, and utilities
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'failure.dart';
|
||||
|
||||
class ErrorMapper {
|
||||
static Failure mapError(dynamic error) {
|
||||
if (error is AuthException) {
|
||||
switch (error.message) {
|
||||
case 'Invalid login credentials':
|
||||
return const AuthFailure('Invalid email or password');
|
||||
case 'Email not confirmed':
|
||||
return const AuthFailure('Please confirm your email address');
|
||||
case 'User already registered':
|
||||
return const AuthFailure('An account with this email already exists');
|
||||
case 'Password should be at least 6 characters':
|
||||
return const AuthFailure('Password must be at least 6 characters');
|
||||
default:
|
||||
return AuthFailure(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (error is PostgrestException) {
|
||||
switch (error.code) {
|
||||
case '23505':
|
||||
return const ValidationFailure('This record already exists');
|
||||
case '23503':
|
||||
return const ValidationFailure('Referenced record does not exist');
|
||||
case '42501':
|
||||
return const PermissionFailure('You do not have permission to perform this action');
|
||||
default:
|
||||
return ServerFailure(
|
||||
error.message,
|
||||
statusCode: int.tryParse(error.code ?? ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (error is Exception) {
|
||||
return NetworkFailure(error.toString().replaceAll('Exception: ', ''));
|
||||
}
|
||||
|
||||
return UnknownFailure(error.toString());
|
||||
}
|
||||
|
||||
static String getUserMessage(Failure failure) {
|
||||
return failure.message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
abstract class Failure {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
const Failure(this.message, {this.statusCode});
|
||||
|
||||
@override
|
||||
String toString() => 'Failure: $message';
|
||||
}
|
||||
|
||||
class ServerFailure extends Failure {
|
||||
const ServerFailure(super.message, {super.statusCode});
|
||||
}
|
||||
|
||||
class NetworkFailure extends Failure {
|
||||
const NetworkFailure(super.message);
|
||||
}
|
||||
|
||||
class AuthFailure extends Failure {
|
||||
const AuthFailure(super.message);
|
||||
}
|
||||
|
||||
class ValidationFailure extends Failure {
|
||||
const ValidationFailure(super.message);
|
||||
}
|
||||
|
||||
class NotFoundFailure extends Failure {
|
||||
const NotFoundFailure(super.message);
|
||||
}
|
||||
|
||||
class PermissionFailure extends Failure {
|
||||
const PermissionFailure(super.message);
|
||||
}
|
||||
|
||||
class UnknownFailure extends Failure {
|
||||
const UnknownFailure(super.message);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateTimeUtils {
|
||||
static const int countdownDays = 1356;
|
||||
|
||||
static DateTime calculateEndDate(DateTime startDate) {
|
||||
return startDate.add(const Duration(days: countdownDays));
|
||||
}
|
||||
|
||||
static Duration calculateRemainingTime(DateTime endDate) {
|
||||
return endDate.difference(DateTime.now());
|
||||
}
|
||||
|
||||
static String formatCountdown(Duration duration) {
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours % 24;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
final seconds = duration.inSeconds % 60;
|
||||
|
||||
return '${days}d ${hours}h ${minutes}m ${seconds}s';
|
||||
}
|
||||
|
||||
static String formatCountdownCompact(Duration duration) {
|
||||
final days = duration.inDays;
|
||||
final hours = duration.inHours % 24;
|
||||
final minutes = duration.inMinutes % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return '${days}d ${hours}h';
|
||||
} else if (hours > 0) {
|
||||
return '${hours}h ${minutes}m';
|
||||
} else {
|
||||
return '${minutes}m';
|
||||
}
|
||||
}
|
||||
|
||||
static double calculateProgress(DateTime startDate, DateTime endDate) {
|
||||
final now = DateTime.now();
|
||||
final totalDuration = endDate.difference(startDate).inSeconds;
|
||||
final elapsedDuration = now.difference(startDate).inSeconds;
|
||||
|
||||
if (elapsedDuration >= totalDuration) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return elapsedDuration / totalDuration;
|
||||
}
|
||||
|
||||
static String formatDate(DateTime date) {
|
||||
return DateFormat('MMM dd, yyyy').format(date);
|
||||
}
|
||||
|
||||
static String formatDateTime(DateTime dateTime) {
|
||||
return DateFormat('MMM dd, yyyy • HH:mm').format(dateTime);
|
||||
}
|
||||
|
||||
static String formatRelativeTime(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inSeconds < 60) {
|
||||
return 'Just now';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes}m ago';
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours}h ago';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays}d ago';
|
||||
} else {
|
||||
return formatDate(dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
static bool isCountdownFinished(DateTime endDate) {
|
||||
return DateTime.now().isAfter(endDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
class Validators {
|
||||
static String? validateEmail(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Email is required';
|
||||
}
|
||||
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(value)) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? validatePassword(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Password is required';
|
||||
}
|
||||
|
||||
if (value.length < 6) {
|
||||
return 'Password must be at least 6 characters';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? validateUsername(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Username is required';
|
||||
}
|
||||
|
||||
if (value.length < 3) {
|
||||
return 'Username must be at least 3 characters';
|
||||
}
|
||||
|
||||
if (value.length > 20) {
|
||||
return 'Username must not exceed 20 characters';
|
||||
}
|
||||
|
||||
final usernameRegex = RegExp(r'^[a-zA-Z0-9_]+$');
|
||||
if (!usernameRegex.hasMatch(value)) {
|
||||
return 'Username can only contain letters, numbers, and underscores';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? validateGoalTitle(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Goal title is required';
|
||||
}
|
||||
|
||||
if (value.length > 100) {
|
||||
return 'Goal title must not exceed 100 characters';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? validateGoalDescription(String? value) {
|
||||
if (value != null && value.length > 500) {
|
||||
return 'Description must not exceed 500 characters';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? validateGoalProgress(int? value) {
|
||||
if (value == null) {
|
||||
return 'Progress is required';
|
||||
}
|
||||
|
||||
if (value < 0 || value > 100) {
|
||||
return 'Progress must be between 0 and 100';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? validateRequired(String? value, String fieldName) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$fieldName is required';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppScaffold extends StatelessWidget {
|
||||
final String? title;
|
||||
final Widget body;
|
||||
final List<Widget>? actions;
|
||||
final Widget? floatingActionButton;
|
||||
final bool showBackButton;
|
||||
final VoidCallback? onBackPressed;
|
||||
final Widget? bottomNavigationBar;
|
||||
|
||||
const AppScaffold({
|
||||
super.key,
|
||||
this.title,
|
||||
required this.body,
|
||||
this.actions,
|
||||
this.floatingActionButton,
|
||||
this.showBackButton = false,
|
||||
this.onBackPressed,
|
||||
this.bottomNavigationBar,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: title != null
|
||||
? AppBar(
|
||||
title: Text(title!),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: showBackButton,
|
||||
leading: showBackButton
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: onBackPressed ?? () => Navigator.pop(context),
|
||||
)
|
||||
: null,
|
||||
actions: actions,
|
||||
)
|
||||
: null,
|
||||
body: SafeArea(child: body),
|
||||
floatingActionButton: floatingActionButton,
|
||||
bottomNavigationBar: bottomNavigationBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class BottomNavScaffold extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const BottomNavScaffold({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BottomNavScaffold> createState() => _BottomNavScaffoldState();
|
||||
}
|
||||
|
||||
class _BottomNavScaffoldState extends State<BottomNavScaffold> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<NavigationDestination> _destinations = const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.flag_outlined),
|
||||
selectedIcon: Icon(Icons.flag),
|
||||
label: 'Goals',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.people_outline),
|
||||
selectedIcon: Icon(Icons.people),
|
||||
label: 'Social',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.person_outline),
|
||||
selectedIcon: Icon(Icons.person),
|
||||
label: 'Profile',
|
||||
),
|
||||
];
|
||||
|
||||
void _onDestinationSelected(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.go('/home');
|
||||
break;
|
||||
case 1:
|
||||
context.go('/goals');
|
||||
break;
|
||||
case 2:
|
||||
context.go('/social');
|
||||
break;
|
||||
case 3:
|
||||
context.go('/profile');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _currentIndex,
|
||||
onDestinationSelected: _onDestinationSelected,
|
||||
destinations: _destinations,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmptyState extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const EmptyState({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: onAction,
|
||||
child: Text(actionLabel!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoadingIndicator extends StatelessWidget {
|
||||
final String? message;
|
||||
|
||||
const LoadingIndicator({
|
||||
super.key,
|
||||
this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user