mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-04 20:12:56 +00:00
Added core data models, repositories, and utilities
This commit is contained in:
@@ -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