Initial commit: Project documentation and git setup

This commit is contained in:
Tomas Dvorak
2026-01-03 18:35:35 +01:00
commit 1639de69d4
51 changed files with 5005 additions and 0 deletions
+15
View File
@@ -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();
}
+11
View File
@@ -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}'),
),
),
);
});
+4
View File
@@ -0,0 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
+161
View File
@@ -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),
),
);
}
}
+81
View File
@@ -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,
];
}
+79
View File
@@ -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'),
),
);
}
}
+37
View File
@@ -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,
);
}
}