Added core data models, repositories, and utilities

This commit is contained in:
Tomas Dvorak
2026-01-03 18:37:48 +01:00
parent 1639de69d4
commit 572fbb971c
17 changed files with 1248 additions and 0 deletions
@@ -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());
}
}