small fix, don't worry about it

This commit is contained in:
Tomas Dvorak
2026-04-10 12:05:40 +02:00
parent 7b7ed0083f
commit 5ab2773f98
55 changed files with 3240 additions and 483 deletions
@@ -145,7 +145,7 @@ class SocialState {
}
final socialRepositoryProvider = Provider<SocialRepository>((ref) {
return SocialRepository(supabaseClient);
return SocialRepository(supabaseClient ?? (throw Exception("Supabase not initialized")));
});
final socialControllerProvider = StateNotifierProvider<SocialController, SocialState>((ref) {
@@ -162,7 +162,7 @@ final socialNotificationsControllerProvider =
});
final socialRepositoryProvider = Provider<SocialRepository>((ref) {
return SocialRepository(supabaseClient);
return SocialRepository(supabaseClient ?? (throw Exception("Supabase not initialized")));
});
final notificationsRepositoryProvider = Provider<NotificationsRepository>((ref) {
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
import '../../../core/widgets/app_scaffold.dart';
import '../../../core/widgets/loading_indicator.dart';
import '../../../core/utils/date_time_utils.dart';
import '../../../core/utils/unit_conversion_utils.dart';
import '../../../data/models/user_model.dart' as app;
import '../../../data/models/goal_model.dart';
import '../../auth/application/auth_controller.dart';
import '../application/social_controller.dart';
import '../../profile/application/profile_controller.dart';
@@ -22,12 +25,19 @@ class PublicProfileScreen extends ConsumerStatefulWidget {
}
class _PublicProfileScreenState extends ConsumerState<PublicProfileScreen> {
Map<String, dynamic>? _userStats;
List<Goal>? _userGoals;
bool _isLoadingStats = false;
bool _isLoadingGoals = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadProfile();
_checkFollowingStatus();
_loadUserStats();
_loadUserGoals();
});
}
@@ -39,6 +49,37 @@ class _PublicProfileScreenState extends ConsumerState<PublicProfileScreen> {
await ref.read(socialControllerProvider.notifier).isFollowing(widget.userId);
}
Future<void> _loadUserStats() async {
setState(() => _isLoadingStats = true);
try {
final client = supabase.Supabase.instance.client;
final response = await client.rpc('get_user_stats', params: {'user_uuid': widget.userId});
setState(() {
_userStats = response as Map<String, dynamic>;
_isLoadingStats = false;
});
} catch (e) {
setState(() => _isLoadingStats = false);
}
}
Future<void> _loadUserGoals() async {
setState(() => _isLoadingGoals = true);
try {
final client = supabase.Supabase.instance.client;
final response = await client.rpc('get_public_user_goals', params: {
'user_uuid': widget.userId,
'limit_count': 10
});
setState(() {
_userGoals = (response as List).map((json) => Goal.fromJson(json)).toList();
_isLoadingGoals = false;
});
} catch (e) {
setState(() => _isLoadingGoals = false);
}
}
Future<void> _toggleFollow() async {
final controller = ref.read(socialControllerProvider.notifier);
final isFollowing = await controller.isFollowing(widget.userId);
@@ -90,6 +131,8 @@ class _PublicProfileScreenState extends ConsumerState<PublicProfileScreen> {
onRefresh: () async {
await _loadProfile();
await _checkFollowingStatus();
await _loadUserStats();
await _loadUserGoals();
},
child: CustomScrollView(
slivers: [
@@ -101,7 +144,116 @@ class _PublicProfileScreenState extends ConsumerState<PublicProfileScreen> {
),
),
SliverToBoxAdapter(
child: _StatsSection(user: user),
child: _EnhancedStatsSection(
user: user,
userStats: _userStats,
isLoadingStats: _isLoadingStats,
),
),
if (_userGoals != null && _userGoals!.isNotEmpty)
SliverToBoxAdapter(
child: _UserGoalsSection(
goals: _userGoals!,
isLoadingGoals: _isLoadingGoals,
),
),
if (user.age != null || user.formattedHeight.isNotEmpty || user.formattedWeight.isNotEmpty)
SliverToBoxAdapter(
child: _BiometricSection(user: user),
),
],
),
);
}
}
class _EnhancedStatsSection extends StatelessWidget {
final app.User user;
final Map<String, dynamic>? userStats;
final bool isLoadingStats;
const _EnhancedStatsSection({
required this.user,
this.userStats,
required this.isLoadingStats,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Profile Stats',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (isLoadingStats)
const Center(child: CircularProgressIndicator())
else
Row(
children: [
Expanded(
child: _StatCard(
icon: Icons.flag,
title: 'Goals',
value: '${userStats?['goals_count'] ?? 0}',
),
),
const SizedBox(width: 12),
Expanded(
child: _StatCard(
icon: Icons.check_circle,
title: 'Completed',
value: '${userStats?['completed_goals_count'] ?? 0}',
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _StatCard(
icon: Icons.people,
title: 'Followers',
value: '${userStats?['followers_count'] ?? 0}',
),
),
const SizedBox(width: 12),
Expanded(
child: _StatCard(
icon: Icons.person_add,
title: 'Following',
value: '${userStats?['following_count'] ?? 0}',
),
),
],
),
const SizedBox(height: 16),
if (user.countdownStartDate != null) ...[
_StatCard(
icon: Icons.timer,
title: 'Challenge Started',
value: DateTimeUtils.formatShortDate(user.countdownStartDate!),
),
const SizedBox(height: 12),
if (user.daysRemaining != null)
_StatCard(
icon: Icons.hourglass_empty,
title: 'Days Remaining',
value: '${user.daysRemaining} days',
),
const SizedBox(height: 12),
],
_StatCard(
icon: Icons.calendar_today,
title: 'Member Since',
value: DateTimeUtils.formatShortDate(user.createdAt),
),
],
),
@@ -109,6 +261,217 @@ class _PublicProfileScreenState extends ConsumerState<PublicProfileScreen> {
}
}
class _UserGoalsSection extends StatelessWidget {
final List<Goal> goals;
final bool isLoadingGoals;
const _UserGoalsSection({
required this.goals,
required this.isLoadingGoals,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Public Goals',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (isLoadingGoals)
const Center(child: CircularProgressIndicator())
else
...goals.map((goal) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _GoalCard(goal: goal),
)),
],
),
);
}
}
class _BiometricSection extends StatelessWidget {
final app.User user;
const _BiometricSection({required this.user});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Biometric Information',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
children: [
if (user.gender != null)
Expanded(
child: _BiometricCard(
icon: user.gender!.emoji,
title: 'Gender',
value: user.gender!.displayName,
),
),
if (user.age != null)
Expanded(
child: _BiometricCard(
icon: '🎂',
title: 'Age',
value: '${user.age} years',
),
),
],
),
if (user.gender != null && user.age != null)
const SizedBox(height: 12),
Row(
children: [
if (user.formattedHeight.isNotEmpty)
Expanded(
child: _BiometricCard(
icon: '📏',
title: 'Height',
value: user.formattedHeight,
),
),
if (user.formattedWeight.isNotEmpty)
Expanded(
child: _BiometricCard(
icon: '⚖️',
title: 'Weight',
value: user.formattedWeight,
),
),
],
),
if (user.bmi != null)
Column(
children: [
const SizedBox(height: 12),
_BiometricCard(
icon: '💪',
title: 'BMI',
value: '${user.bmi!.toStringAsFixed(1)} - ${user.bmiCategory}',
valueColor: UnitConversionUtils.getBmiColor(user.bmi!),
),
],
),
],
),
);
}
}
class _BiometricCard extends StatelessWidget {
final String icon;
final String title;
final String value;
final Color? valueColor;
const _BiometricCard({
required this.icon,
required this.title,
required this.value,
this.valueColor,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
icon,
style: const TextStyle(fontSize: 20),
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: valueColor ?? null,
),
),
),
],
),
],
),
);
}
}
class _GoalCard extends StatelessWidget {
final Goal goal;
const _GoalCard({required this.goal});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: goal.completed
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Icon(
goal.completed ? Icons.check : Icons.flag_outlined,
color: goal.completed
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
title: Text(
goal.title,
style: TextStyle(
decoration: goal.completed ? TextDecoration.lineThrough : null,
),
),
subtitle: goal.description != null && goal.description!.isNotEmpty
? Text(
goal.description!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: null,
trailing: goal.progress > 0
? Text('${goal.progress}%')
: null,
),
);
}
}
class _ProfileHeader extends ConsumerWidget {
final app.User user;
final bool isOwnProfile;
@@ -219,51 +582,6 @@ class _FollowButton extends StatelessWidget {
}
}
class _StatsSection extends StatelessWidget {
final app.User user;
const _StatsSection({required this.user});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Journey Stats',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (user.countdownStartDate != null) ...[
_StatCard(
icon: Icons.timer,
title: 'Challenge Started',
value: DateTimeUtils.formatShortDate(user.countdownStartDate!),
),
const SizedBox(height: 12),
if (user.daysRemaining != null)
_StatCard(
icon: Icons.hourglass_empty,
title: 'Days Remaining',
value: '${user.daysRemaining} days',
),
const SizedBox(height: 12),
],
_StatCard(
icon: Icons.calendar_today,
title: 'Member Since',
value: DateTimeUtils.formatShortDate(user.createdAt),
),
],
),
);
}
}
class _StatCard extends StatelessWidget {
final IconData icon;
final String title;