mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-05 04:22:55 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user