mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-03 19:42:57 +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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class Activity extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String type;
|
||||||
|
final Map<String, dynamic>? payload;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const Activity({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
required this.type,
|
||||||
|
this.payload,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
Activity copyWith({
|
||||||
|
String? id,
|
||||||
|
String? userId,
|
||||||
|
String? type,
|
||||||
|
Map<String, dynamic>? payload,
|
||||||
|
DateTime? createdAt,
|
||||||
|
}) {
|
||||||
|
return Activity(
|
||||||
|
id: id ?? this.id,
|
||||||
|
userId: userId ?? this.userId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
payload: payload ?? this.payload,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'user_id': userId,
|
||||||
|
'type': type,
|
||||||
|
'payload': payload,
|
||||||
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Activity.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Activity(
|
||||||
|
id: json['id'] as String,
|
||||||
|
userId: json['user_id'] as String,
|
||||||
|
type: json['type'] as String,
|
||||||
|
payload: json['payload'] as Map<String, dynamic>?,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, userId, type, payload, createdAt];
|
||||||
|
}
|
||||||
@@ -78,4 +78,38 @@ class Goal extends Equatable {
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'owner_id': ownerId,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'progress': progress,
|
||||||
|
'location_lat': locationLat,
|
||||||
|
'location_lng': locationLng,
|
||||||
|
'location_name': locationName,
|
||||||
|
'image_url': imageUrl,
|
||||||
|
'completed': completed,
|
||||||
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Goal.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Goal(
|
||||||
|
id: json['id'] as String,
|
||||||
|
ownerId: json['owner_id'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
progress: json['progress'] as int? ?? 0,
|
||||||
|
locationLat: json['location_lat'] as double?,
|
||||||
|
locationLng: json['location_lng'] as double?,
|
||||||
|
locationName: json['location_name'] as String?,
|
||||||
|
imageUrl: json['image_url'] as String?,
|
||||||
|
completed: json['completed'] as bool? ?? false,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class GoalStep extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String goalId;
|
||||||
|
final String title;
|
||||||
|
final bool isDone;
|
||||||
|
final int orderIndex;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const GoalStep({
|
||||||
|
required this.id,
|
||||||
|
required this.goalId,
|
||||||
|
required this.title,
|
||||||
|
required this.isDone,
|
||||||
|
required this.orderIndex,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
GoalStep copyWith({
|
||||||
|
String? id,
|
||||||
|
String? goalId,
|
||||||
|
String? title,
|
||||||
|
bool? isDone,
|
||||||
|
int? orderIndex,
|
||||||
|
DateTime? createdAt,
|
||||||
|
}) {
|
||||||
|
return GoalStep(
|
||||||
|
id: id ?? this.id,
|
||||||
|
goalId: goalId ?? this.goalId,
|
||||||
|
title: title ?? this.title,
|
||||||
|
isDone: isDone ?? this.isDone,
|
||||||
|
orderIndex: orderIndex ?? this.orderIndex,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'goal_id': goalId,
|
||||||
|
'title': title,
|
||||||
|
'is_done': isDone,
|
||||||
|
'order_index': orderIndex,
|
||||||
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory GoalStep.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GoalStep(
|
||||||
|
id: json['id'] as String,
|
||||||
|
goalId: json['goal_id'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
isDone: json['is_done'] as bool? ?? false,
|
||||||
|
orderIndex: json['order_index'] as int? ?? 0,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, goalId, title, isDone, orderIndex, createdAt];
|
||||||
|
}
|
||||||
@@ -76,4 +76,38 @@ class User extends Equatable {
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'username': username,
|
||||||
|
'email': email,
|
||||||
|
'avatar_url': avatarUrl,
|
||||||
|
'bio': bio,
|
||||||
|
'is_public_profile': isPublicProfile,
|
||||||
|
'countdown_start_date': countdownStartDate?.toIso8601String(),
|
||||||
|
'countdown_end_date': countdownEndDate?.toIso8601String(),
|
||||||
|
'created_at': createdAt.toIso8601String(),
|
||||||
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory User.fromJson(Map<String, dynamic> json) {
|
||||||
|
return User(
|
||||||
|
id: json['id'] as String,
|
||||||
|
username: json['username'] as String,
|
||||||
|
email: json['email'] as String,
|
||||||
|
avatarUrl: json['avatar_url'] as String?,
|
||||||
|
bio: json['bio'] as String?,
|
||||||
|
isPublicProfile: json['is_public_profile'] as bool? ?? false,
|
||||||
|
countdownStartDate: json['countdown_start_date'] != null
|
||||||
|
? DateTime.parse(json['countdown_start_date'] as String)
|
||||||
|
: null,
|
||||||
|
countdownEndDate: json['countdown_end_date'] != null
|
||||||
|
? DateTime.parse(json['countdown_end_date'] as String)
|
||||||
|
: null,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
|
||||||
|
import '../models/user_model.dart' as app;
|
||||||
|
import '../../core/utils/date_time_utils.dart';
|
||||||
|
import '../../core/errors/failure.dart';
|
||||||
|
|
||||||
|
class CountdownRepository {
|
||||||
|
final supabase.SupabaseClient _client;
|
||||||
|
|
||||||
|
CountdownRepository(this._client);
|
||||||
|
|
||||||
|
Future<app.User> startCountdown(String userId) async {
|
||||||
|
try {
|
||||||
|
final startDate = DateTime.now();
|
||||||
|
final endDate = DateTimeUtils.calculateEndDate(startDate);
|
||||||
|
|
||||||
|
final response = await _client
|
||||||
|
.from('users')
|
||||||
|
.update({
|
||||||
|
'countdown_start_date': startDate.toIso8601String(),
|
||||||
|
'countdown_end_date': endDate.toIso8601String(),
|
||||||
|
'updated_at': DateTime.now().toIso8601String(),
|
||||||
|
})
|
||||||
|
.eq('id', userId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return app.User.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<app.User> getCountdownInfo(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('users')
|
||||||
|
.select()
|
||||||
|
.eq('id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return app.User.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasCountdownStarted(String userId) async {
|
||||||
|
try {
|
||||||
|
final user = await getCountdownInfo(userId);
|
||||||
|
return user.countdownStartDate != null;
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Failure _handleError(dynamic error) {
|
||||||
|
if (error is supabase.PostgrestException) {
|
||||||
|
return ServerFailure(error.message);
|
||||||
|
}
|
||||||
|
return UnknownFailure(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
|
||||||
|
import '../models/goal_model.dart';
|
||||||
|
import '../models/goal_step_model.dart';
|
||||||
|
import '../../core/errors/failure.dart';
|
||||||
|
|
||||||
|
class GoalsRepository {
|
||||||
|
final supabase.SupabaseClient _client;
|
||||||
|
static const int maxGoals = 20;
|
||||||
|
|
||||||
|
GoalsRepository(this._client);
|
||||||
|
|
||||||
|
Future<List<Goal>> getGoals(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('goals')
|
||||||
|
.select()
|
||||||
|
.eq('owner_id', userId)
|
||||||
|
.order('created_at', ascending: false);
|
||||||
|
|
||||||
|
return (response as List).map((json) => Goal.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Goal> getGoal(String goalId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('goals')
|
||||||
|
.select()
|
||||||
|
.eq('id', goalId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return Goal.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Goal> createGoal(Goal goal) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('goals')
|
||||||
|
.insert(goal.toJson())
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return Goal.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Goal> updateGoal(Goal goal) async {
|
||||||
|
try {
|
||||||
|
final updates = goal.toJson();
|
||||||
|
updates['updated_at'] = DateTime.now().toIso8601String();
|
||||||
|
|
||||||
|
final response = await _client
|
||||||
|
.from('goals')
|
||||||
|
.update(updates)
|
||||||
|
.eq('id', goal.id)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return Goal.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteGoal(String goalId) async {
|
||||||
|
try {
|
||||||
|
await _client.from('goals').delete().eq('id', goalId);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<GoalStep>> getGoalSteps(String goalId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('goal_steps')
|
||||||
|
.select()
|
||||||
|
.eq('goal_id', goalId)
|
||||||
|
.order('order_index', ascending: true);
|
||||||
|
|
||||||
|
return (response as List).map((json) => GoalStep.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<GoalStep> createGoalStep(GoalStep step) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('goal_steps')
|
||||||
|
.insert(step.toJson())
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return GoalStep.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<GoalStep> updateGoalStep(GoalStep step) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('goal_steps')
|
||||||
|
.update(step.toJson())
|
||||||
|
.eq('id', step.id)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return GoalStep.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteGoalStep(String stepId) async {
|
||||||
|
try {
|
||||||
|
await _client.from('goal_steps').delete().eq('id', stepId);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getGoalsCount(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('goals')
|
||||||
|
.select('id')
|
||||||
|
.eq('owner_id', userId);
|
||||||
|
|
||||||
|
return (response as List).length;
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Failure _handleError(dynamic error) {
|
||||||
|
if (error is supabase.PostgrestException) {
|
||||||
|
if (error.code == '23505') {
|
||||||
|
return const ValidationFailure('A goal with this title already exists');
|
||||||
|
}
|
||||||
|
return ServerFailure(error.message);
|
||||||
|
}
|
||||||
|
return UnknownFailure(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
|
import 'package:timezone/data/latest.dart' as tz_data;
|
||||||
|
|
||||||
|
class NotificationsRepository {
|
||||||
|
final FlutterLocalNotificationsPlugin _notificationsPlugin;
|
||||||
|
|
||||||
|
NotificationsRepository() : _notificationsPlugin = FlutterLocalNotificationsPlugin() {
|
||||||
|
_initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initialize() async {
|
||||||
|
tz_data.initializeTimeZones();
|
||||||
|
|
||||||
|
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
const iosSettings = DarwinInitializationSettings();
|
||||||
|
|
||||||
|
const initSettings = InitializationSettings(
|
||||||
|
android: androidSettings,
|
||||||
|
iOS: iosSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notificationsPlugin.initialize(initSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showNotification({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
String? payload,
|
||||||
|
}) async {
|
||||||
|
const androidDetails = AndroidNotificationDetails(
|
||||||
|
'lifetimer_channel',
|
||||||
|
'LifeTimer Notifications',
|
||||||
|
channelDescription: 'Notifications for LifeTimer app',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
);
|
||||||
|
|
||||||
|
const iosDetails = DarwinNotificationDetails();
|
||||||
|
|
||||||
|
const notificationDetails = NotificationDetails(
|
||||||
|
android: androidDetails,
|
||||||
|
iOS: iosDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notificationsPlugin.show(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
notificationDetails,
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scheduleNotification({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
required DateTime scheduledDate,
|
||||||
|
String? payload,
|
||||||
|
}) async {
|
||||||
|
const androidDetails = AndroidNotificationDetails(
|
||||||
|
'lifetimer_channel',
|
||||||
|
'LifeTimer Notifications',
|
||||||
|
channelDescription: 'Notifications for LifeTimer app',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
);
|
||||||
|
|
||||||
|
const iosDetails = DarwinNotificationDetails();
|
||||||
|
|
||||||
|
const notificationDetails = NotificationDetails(
|
||||||
|
android: androidDetails,
|
||||||
|
iOS: iosDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notificationsPlugin.zonedSchedule(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
tz.TZDateTime.from(scheduledDate, tz.local),
|
||||||
|
notificationDetails,
|
||||||
|
uiLocalNotificationDateInterpretation:
|
||||||
|
UILocalNotificationDateInterpretation.absoluteTime,
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scheduleDailyReminder({
|
||||||
|
required int id,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
required int hour,
|
||||||
|
required int minute,
|
||||||
|
}) async {
|
||||||
|
const androidDetails = AndroidNotificationDetails(
|
||||||
|
'lifetimer_daily_channel',
|
||||||
|
'Daily Reminders',
|
||||||
|
channelDescription: 'Daily reminder notifications',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
);
|
||||||
|
|
||||||
|
const iosDetails = DarwinNotificationDetails();
|
||||||
|
|
||||||
|
const notificationDetails = NotificationDetails(
|
||||||
|
android: androidDetails,
|
||||||
|
iOS: iosDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _notificationsPlugin.zonedSchedule(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
_nextInstanceOfTime(hour, minute),
|
||||||
|
notificationDetails,
|
||||||
|
uiLocalNotificationDateInterpretation:
|
||||||
|
UILocalNotificationDateInterpretation.absoluteTime,
|
||||||
|
matchDateTimeComponents: DateTimeComponents.time,
|
||||||
|
payload: 'daily_reminder',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelNotification(int id) async {
|
||||||
|
await _notificationsPlugin.cancel(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelAllNotifications() async {
|
||||||
|
await _notificationsPlugin.cancelAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
|
||||||
|
return await _notificationsPlugin.pendingNotificationRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
|
||||||
|
final now = tz.TZDateTime.now(tz.local);
|
||||||
|
final scheduledDate = tz.TZDateTime(
|
||||||
|
tz.local,
|
||||||
|
now.year,
|
||||||
|
now.month,
|
||||||
|
now.day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scheduledDate.isBefore(now)) {
|
||||||
|
return scheduledDate.add(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduledDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
|
||||||
|
import '../models/user_model.dart' as app;
|
||||||
|
import '../models/activity_model.dart';
|
||||||
|
import '../../core/errors/failure.dart';
|
||||||
|
|
||||||
|
class SocialRepository {
|
||||||
|
final supabase.SupabaseClient _client;
|
||||||
|
|
||||||
|
SocialRepository(this._client);
|
||||||
|
|
||||||
|
Future<void> followUser(String userId, String targetUserId) async {
|
||||||
|
try {
|
||||||
|
await _client.from('followers').insert({
|
||||||
|
'user_id': targetUserId,
|
||||||
|
'follower_id': userId,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unfollowUser(String userId, String targetUserId) async {
|
||||||
|
try {
|
||||||
|
await _client
|
||||||
|
.from('followers')
|
||||||
|
.delete()
|
||||||
|
.eq('user_id', targetUserId)
|
||||||
|
.eq('follower_id', userId);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isFollowing(String userId, String targetUserId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('followers')
|
||||||
|
.select('id')
|
||||||
|
.eq('user_id', targetUserId)
|
||||||
|
.eq('follower_id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
return response != null;
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<app.User>> getFollowers(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('followers')
|
||||||
|
.select('follower_id, users!followers_follower_id_fkey(*)')
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
return (response as List)
|
||||||
|
.map((json) => app.User.fromJson(json['users']))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<app.User>> getFollowing(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('followers')
|
||||||
|
.select('user_id, users!followers_user_id_fkey(*)')
|
||||||
|
.eq('follower_id', userId);
|
||||||
|
|
||||||
|
return (response as List)
|
||||||
|
.map((json) => app.User.fromJson(json['users']))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Activity>> getActivityFeed(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('activities')
|
||||||
|
.select()
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.order('created_at', ascending: false)
|
||||||
|
.limit(50);
|
||||||
|
|
||||||
|
return (response as List).map((json) => Activity.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Activity> logActivity({
|
||||||
|
required String userId,
|
||||||
|
required String type,
|
||||||
|
Map<String, dynamic>? payload,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('activities')
|
||||||
|
.insert({
|
||||||
|
'user_id': userId,
|
||||||
|
'type': type,
|
||||||
|
'payload': payload,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return Activity.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<app.User>> getLeaderboard({
|
||||||
|
required String sortBy,
|
||||||
|
int limit = 50,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
String orderBy;
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'goals_completed':
|
||||||
|
orderBy = 'goals_completed_count';
|
||||||
|
break;
|
||||||
|
case 'streak':
|
||||||
|
orderBy = 'streak_days';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
orderBy = 'created_at';
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _client
|
||||||
|
.from('users')
|
||||||
|
.select()
|
||||||
|
.eq('is_public_profile', true)
|
||||||
|
.order(orderBy, ascending: false)
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
return (response as List).map((json) => app.User.fromJson(json)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Failure _handleError(dynamic error) {
|
||||||
|
if (error is supabase.PostgrestException) {
|
||||||
|
return ServerFailure(error.message);
|
||||||
|
}
|
||||||
|
return UnknownFailure(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
|
||||||
|
import '../models/user_model.dart' as app;
|
||||||
|
import '../../core/errors/failure.dart';
|
||||||
|
|
||||||
|
class UserRepository {
|
||||||
|
final supabase.SupabaseClient _client;
|
||||||
|
|
||||||
|
UserRepository(this._client);
|
||||||
|
|
||||||
|
Future<app.User> getProfile(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('users')
|
||||||
|
.select()
|
||||||
|
.eq('id', userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return app.User.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<app.User> updateProfile({
|
||||||
|
required String userId,
|
||||||
|
String? username,
|
||||||
|
String? avatarUrl,
|
||||||
|
String? bio,
|
||||||
|
bool? isPublicProfile,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final updates = <String, dynamic>{};
|
||||||
|
if (username != null) updates['username'] = username;
|
||||||
|
if (avatarUrl != null) updates['avatar_url'] = avatarUrl;
|
||||||
|
if (bio != null) updates['bio'] = bio;
|
||||||
|
if (isPublicProfile != null) updates['is_public_profile'] = isPublicProfile;
|
||||||
|
updates['updated_at'] = DateTime.now().toIso8601String();
|
||||||
|
|
||||||
|
final response = await _client
|
||||||
|
.from('users')
|
||||||
|
.update(updates)
|
||||||
|
.eq('id', userId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return app.User.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isUsernameAvailable(String username) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('users')
|
||||||
|
.select('id')
|
||||||
|
.eq('username', username)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
return response == null;
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAccount(String userId) async {
|
||||||
|
try {
|
||||||
|
await _client.from('users').delete().eq('id', userId);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Failure _handleError(dynamic error) {
|
||||||
|
if (error is supabase.PostgrestException) {
|
||||||
|
if (error.code == '23505') {
|
||||||
|
return const ValidationFailure('Username already taken');
|
||||||
|
}
|
||||||
|
return ServerFailure(error.message);
|
||||||
|
}
|
||||||
|
return UnknownFailure(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user