mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-03 19:42:57 +00:00
Initial commit: Project documentation and git setup
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'env.dart';
|
||||
import 'supabase_client.dart';
|
||||
|
||||
Future<void> bootstrap() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await Supabase.initialize(
|
||||
url: Env.supabaseUrl,
|
||||
anonKey: Env.supabaseAnonKey,
|
||||
);
|
||||
|
||||
initializeSupabaseClient();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
class Env {
|
||||
static const String supabaseUrl = String.fromEnvironment(
|
||||
'SUPABASE_URL',
|
||||
defaultValue: 'https://your-project.supabase.co',
|
||||
);
|
||||
|
||||
static const String supabaseAnonKey = String.fromEnvironment(
|
||||
'SUPABASE_ANON_KEY',
|
||||
defaultValue: 'your-anon-key',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
void initializeSupabaseClient() {
|
||||
// Additional client setup if needed
|
||||
// For now, we use the default Supabase.instance.client
|
||||
}
|
||||
|
||||
SupabaseClient get supabaseClient => Supabase.instance.client;
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../features/auth/presentation/auth_gate.dart';
|
||||
import '../../features/onboarding/presentation/onboarding_intro_screen.dart';
|
||||
import '../../features/countdown/presentation/home_countdown_screen.dart';
|
||||
import '../../features/goals/presentation/goals_list_screen.dart';
|
||||
import '../../features/social/presentation/social_feed_screen.dart';
|
||||
import '../../features/profile/presentation/profile_screen.dart';
|
||||
import '../../features/settings/presentation/settings_home_screen.dart';
|
||||
|
||||
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
return GoRouter(
|
||||
initialLocation: '/',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const AuthGate(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/onboarding',
|
||||
builder: (context, state) => const OnboardingIntroScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
builder: (context, state) => const HomeCountdownScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/goals',
|
||||
builder: (context, state) => const GoalsListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/social',
|
||||
builder: (context, state) => const SocialFeedScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
builder: (context, state) => const SettingsHomeScreen(),
|
||||
),
|
||||
],
|
||||
errorBuilder: (context, state) => Scaffold(
|
||||
body: Center(
|
||||
child: Text('Error: ${state.error}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
|
||||
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AppTheme {
|
||||
static const Color primaryColor = Color(0xFF6366F1);
|
||||
static const Color secondaryColor = Color(0xFF8B5CF6);
|
||||
static const Color accentColor = Color(0xFFEC4899);
|
||||
static const Color backgroundColor = Color(0xFFFAFAFA);
|
||||
static const Color surfaceColor = Color(0xFFFFFFFF);
|
||||
static const Color errorColor = Color(0xFFEF4444);
|
||||
static const Color warningColor = Color(0xFFF59E0B);
|
||||
static const Color successColor = Color(0xFF10B981);
|
||||
|
||||
static const ColorScheme lightColorScheme = ColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primary: primaryColor,
|
||||
onPrimary: Color(0xFFFFFFFF),
|
||||
secondary: secondaryColor,
|
||||
onSecondary: Color(0xFFFFFFFF),
|
||||
error: errorColor,
|
||||
onError: Color(0xFFFFFFFF),
|
||||
surface: surfaceColor,
|
||||
onSurface: Color(0xFF1F2937),
|
||||
background: backgroundColor,
|
||||
onBackground: Color(0xFF1F2937),
|
||||
);
|
||||
|
||||
static const ColorScheme darkColorScheme = ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primary: primaryColor,
|
||||
onPrimary: Color(0xFFFFFFFF),
|
||||
secondary: secondaryColor,
|
||||
onSecondary: Color(0xFFFFFFFF),
|
||||
error: errorColor,
|
||||
onError: Color(0xFFFFFFFF),
|
||||
surface: Color(0xFF1F2937),
|
||||
onSurface: Color(0xFFF9FAFB),
|
||||
background: Color(0xFF111827),
|
||||
onBackground: Color(0xFFF9FAFB),
|
||||
);
|
||||
|
||||
static ThemeData get light {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: lightColorScheme,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: surfaceColor,
|
||||
foregroundColor: Color(0xFF1F2937),
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: surfaceColor,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Color(0xFFFFFFFF),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
headlineLarge: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF4B5563),
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData get dark {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: darkColorScheme,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xFF1F2937),
|
||||
foregroundColor: Color(0xFFF9FAFB),
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: const Color(0xFF374151),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Color(0xFFFFFFFF),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFF9FAFB),
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFF9FAFB),
|
||||
),
|
||||
headlineLarge: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFF9FAFB),
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFF9FAFB),
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFFD1D5DB),
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF9CA3AF),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback onPressed;
|
||||
final bool isLoading;
|
||||
final bool isDisabled;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const PrimaryButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.isDisabled = false,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height ?? 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: (isLoading || isDisabled) ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
foregroundColor ?? Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(text),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Goal extends Equatable {
|
||||
final String id;
|
||||
final String ownerId;
|
||||
final String title;
|
||||
final String? description;
|
||||
final int progress;
|
||||
final double? locationLat;
|
||||
final double? locationLng;
|
||||
final String? locationName;
|
||||
final String? imageUrl;
|
||||
final bool completed;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Goal({
|
||||
required this.id,
|
||||
required this.ownerId,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.progress = 0,
|
||||
this.locationLat,
|
||||
this.locationLng,
|
||||
this.locationName,
|
||||
this.imageUrl,
|
||||
this.completed = false,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
bool get hasLocation => locationLat != null && locationLng != null;
|
||||
|
||||
bool get hasImage => imageUrl != null && imageUrl!.isNotEmpty;
|
||||
|
||||
Goal copyWith({
|
||||
String? id,
|
||||
String? ownerId,
|
||||
String? title,
|
||||
String? description,
|
||||
int? progress,
|
||||
double? locationLat,
|
||||
double? locationLng,
|
||||
String? locationName,
|
||||
String? imageUrl,
|
||||
bool? completed,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return Goal(
|
||||
id: id ?? this.id,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
progress: progress ?? this.progress,
|
||||
locationLat: locationLat ?? this.locationLat,
|
||||
locationLng: locationLng ?? this.locationLng,
|
||||
locationName: locationName ?? this.locationName,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
completed: completed ?? this.completed,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
ownerId,
|
||||
title,
|
||||
description,
|
||||
progress,
|
||||
locationLat,
|
||||
locationLng,
|
||||
locationName,
|
||||
imageUrl,
|
||||
completed,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class User extends Equatable {
|
||||
final String id;
|
||||
final String username;
|
||||
final String email;
|
||||
final String? avatarUrl;
|
||||
final String? bio;
|
||||
final bool isPublicProfile;
|
||||
final DateTime? countdownStartDate;
|
||||
final DateTime? countdownEndDate;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.email,
|
||||
this.avatarUrl,
|
||||
this.bio,
|
||||
this.isPublicProfile = false,
|
||||
this.countdownStartDate,
|
||||
this.countdownEndDate,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
bool get hasCountdownStarted => countdownStartDate != null;
|
||||
|
||||
bool get isCountdownActive {
|
||||
if (!hasCountdownStarted || countdownEndDate == null) return false;
|
||||
return DateTime.now().isBefore(countdownEndDate!);
|
||||
}
|
||||
|
||||
int? get daysRemaining {
|
||||
if (!isCountdownActive) return null;
|
||||
return countdownEndDate!.difference(DateTime.now()).inDays;
|
||||
}
|
||||
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? username,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
String? bio,
|
||||
bool? isPublicProfile,
|
||||
DateTime? countdownStartDate,
|
||||
DateTime? countdownEndDate,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
username: username ?? this.username,
|
||||
email: email ?? this.email,
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
bio: bio ?? this.bio,
|
||||
isPublicProfile: isPublicProfile ?? this.isPublicProfile,
|
||||
countdownStartDate: countdownStartDate ?? this.countdownStartDate,
|
||||
countdownEndDate: countdownEndDate ?? this.countdownEndDate,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
avatarUrl,
|
||||
bio,
|
||||
isPublicProfile,
|
||||
countdownStartDate,
|
||||
countdownEndDate,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import '../models/user_model.dart';
|
||||
import '../../bootstrap/supabase_client.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart' hide User;
|
||||
|
||||
class AuthRepository {
|
||||
final SupabaseClient _client;
|
||||
|
||||
AuthRepository([SupabaseClient? client]) : _client = client ?? supabaseClient;
|
||||
|
||||
Stream<User?> get authStateChanges {
|
||||
return _client.auth.onAuthStateChange.map((data) {
|
||||
final session = data.session;
|
||||
if (session?.user != null) {
|
||||
return _mapSupabaseUserToAppUser(session!.user);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
User? get currentUser {
|
||||
final user = _client.auth.currentUser;
|
||||
return user != null ? _mapSupabaseUserToAppUser(user) : null;
|
||||
}
|
||||
|
||||
Future<void> signInWithEmail(String email, String password) async {
|
||||
await _client.auth.signInWithPassword(email: email, password: password);
|
||||
}
|
||||
|
||||
Future<void> signUpWithEmail(String email, String password, String username) async {
|
||||
final response = await _client.auth.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
data: {'username': username},
|
||||
);
|
||||
|
||||
if (response.user != null) {
|
||||
await _createUserProfile(response.user!.id, username, email);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signInWithGoogle() async {
|
||||
// TODO: Implement Google OAuth
|
||||
// await _client.auth.signInWithOAuth(OAuthProvider.google);
|
||||
throw UnimplementedError('Google OAuth not implemented yet');
|
||||
}
|
||||
|
||||
Future<void> signInWithApple() async {
|
||||
// TODO: Implement Apple OAuth
|
||||
// await _client.auth.signInWithOAuth(OAuthProvider.apple);
|
||||
throw UnimplementedError('Apple OAuth not implemented yet');
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
await _client.auth.signOut();
|
||||
}
|
||||
|
||||
Future<void> resetPassword(String email) async {
|
||||
await _client.auth.resetPasswordForEmail(email);
|
||||
}
|
||||
|
||||
Future<void> updateProfile({
|
||||
String? username,
|
||||
String? bio,
|
||||
String? avatarUrl,
|
||||
bool? isPublicProfile,
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('User not authenticated');
|
||||
|
||||
final updates = <String, dynamic>{};
|
||||
if (username != null) updates['username'] = username;
|
||||
if (bio != null) updates['bio'] = bio;
|
||||
if (avatarUrl != null) updates['avatar_url'] = avatarUrl;
|
||||
if (isPublicProfile != null) updates['is_public_profile'] = isPublicProfile;
|
||||
updates['updated_at'] = DateTime.now().toIso8601String();
|
||||
|
||||
await _client
|
||||
.from('users')
|
||||
.update(updates)
|
||||
.eq('id', userId);
|
||||
}
|
||||
|
||||
Future<User> _createUserProfile(String userId, String username, String email) async {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
|
||||
final response = await _client.from('users').insert({
|
||||
'id': userId,
|
||||
'username': username,
|
||||
'email': email,
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
}).select().single();
|
||||
|
||||
return _mapSupabaseDataToUser(response);
|
||||
}
|
||||
|
||||
User _mapSupabaseUserToAppUser(dynamic supabaseUser) {
|
||||
return User(
|
||||
id: supabaseUser.id,
|
||||
username: supabaseUser.userMetadata?['username'] ?? '',
|
||||
email: supabaseUser.email ?? '',
|
||||
createdAt: DateTime.tryParse(supabaseUser.createdAt ?? '') ?? DateTime.now(),
|
||||
updatedAt: DateTime.tryParse(supabaseUser.updatedAt ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
User _mapSupabaseDataToUser(Map<String, dynamic> data) {
|
||||
return User(
|
||||
id: data['id'],
|
||||
username: data['username'],
|
||||
email: data['email'],
|
||||
avatarUrl: data['avatar_url'],
|
||||
bio: data['bio'],
|
||||
isPublicProfile: data['is_public_profile'] ?? false,
|
||||
countdownStartDate: data['countdown_start_date'] != null
|
||||
? DateTime.parse(data['countdown_start_date'])
|
||||
: null,
|
||||
countdownEndDate: data['countdown_end_date'] != null
|
||||
? DateTime.parse(data['countdown_end_date'])
|
||||
: null,
|
||||
createdAt: DateTime.parse(data['created_at']),
|
||||
updatedAt: DateTime.parse(data['updated_at']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../data/repositories/auth_repository.dart';
|
||||
import '../../../data/models/user_model.dart';
|
||||
|
||||
final authControllerProvider = StateNotifierProvider<AuthController, User?>((ref) {
|
||||
return AuthController(ref.read(authRepositoryProvider));
|
||||
});
|
||||
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
return AuthRepository(/* SupabaseClient instance will be injected */);
|
||||
});
|
||||
|
||||
class AuthController extends StateNotifier<User?> {
|
||||
final AuthRepository _authRepository;
|
||||
|
||||
AuthController(this._authRepository) : super(null) {
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() {
|
||||
state = _authRepository.currentUser;
|
||||
_authRepository.authStateChanges.listen((user) {
|
||||
state = user;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> signInWithEmail(String email, String password) async {
|
||||
await _authRepository.signInWithEmail(email, password);
|
||||
}
|
||||
|
||||
Future<void> signUpWithEmail(String email, String password, String username) async {
|
||||
await _authRepository.signUpWithEmail(email, password, username);
|
||||
}
|
||||
|
||||
Future<void> signInWithGoogle() async {
|
||||
await _authRepository.signInWithGoogle();
|
||||
}
|
||||
|
||||
Future<void> signInWithApple() async {
|
||||
await _authRepository.signInWithApple();
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
await _authRepository.signOut();
|
||||
}
|
||||
|
||||
Future<void> resetPassword(String email) async {
|
||||
await _authRepository.resetPassword(email);
|
||||
}
|
||||
|
||||
Future<void> updateProfile({
|
||||
String? username,
|
||||
String? bio,
|
||||
String? avatarUrl,
|
||||
bool? isPublicProfile,
|
||||
}) async {
|
||||
await _authRepository.updateProfile(
|
||||
username: username,
|
||||
bio: bio,
|
||||
avatarUrl: avatarUrl,
|
||||
isPublicProfile: isPublicProfile,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../application/auth_controller.dart';
|
||||
import '../../onboarding/presentation/onboarding_intro_screen.dart';
|
||||
|
||||
class AuthGate extends ConsumerWidget {
|
||||
const AuthGate({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final authState = ref.watch(authControllerProvider);
|
||||
|
||||
if (authState == null) {
|
||||
return const SignInScreen();
|
||||
}
|
||||
|
||||
return const OnboardingIntroScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class SignInScreen extends ConsumerStatefulWidget {
|
||||
const SignInScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<SignInScreen> createState() => _SignInScreenState();
|
||||
}
|
||||
|
||||
class _SignInScreenState extends ConsumerState<SignInScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _signIn() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
await ref.read(authControllerProvider.notifier).signInWithEmail(
|
||||
_emailController.text.trim(),
|
||||
_passwordController.text,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('LifeTimer'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Welcome Back',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _signIn,
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text('Sign In'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigate to sign up
|
||||
},
|
||||
child: const Text('Don\'t have an account? Sign Up'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeCountdownScreen extends StatelessWidget {
|
||||
const HomeCountdownScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('LifeTimer'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'1356',
|
||||
style: TextStyle(
|
||||
fontSize: 72,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'days remaining',
|
||||
style: TextStyle(fontSize: 24),
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
Text(
|
||||
'Your countdown starts here',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GoalsListScreen extends StatelessWidget {
|
||||
const GoalsListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Goals'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Goals List - Coming Soon'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OnboardingIntroScreen extends StatelessWidget {
|
||||
const OnboardingIntroScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('LifeTimer'),
|
||||
),
|
||||
body: const Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Welcome to LifeTimer',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Your 1356-day journey starts here.\nCreate your bucket list and begin your countdown.',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Profile'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Profile - Coming Soon'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsHomeScreen extends StatelessWidget {
|
||||
const SettingsHomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Settings - Coming Soon'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SocialFeedScreen extends StatelessWidget {
|
||||
const SocialFeedScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Social'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Social Feed - Coming Soon'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'bootstrap/bootstrap.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'core/routing/app_router.dart';
|
||||
import 'core/state/providers.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await bootstrap();
|
||||
|
||||
runApp(
|
||||
const ProviderScope(
|
||||
child: LifeTimerApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class LifeTimerApp extends ConsumerWidget {
|
||||
const LifeTimerApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'LifeTimer',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: themeMode,
|
||||
routerConfig: router,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user