feat(share): rebuild share rank with dart:ui Canvas generator
Replace RepaintBoundary widget capture approach with a pure dart:ui PictureRecorder + Canvas implementation. - Add share_card_generator.dart: generates 1080×1920 PNG via Canvas without embedding any widget in the tree - Remove share_rank_card.dart (widget approach no longer needed) - Remove GlobalKey, _buildHiddenShareCard, RepaintBoundary, _fmtEp from profile_screen.dart - Simplify desktop + mobile Stacks to direct ScrollViews - Fix Android GPU compositing timing crash (no retry needed) - Add avatarImage.dispose() to prevent GPU memory leak - Guard byteData null return with StateError - Replace MaterialIcons bolt with Unicode ⚡ (tree-shake safe) - Align tier in share text with tier rendered on card Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -20,10 +22,11 @@ import '../widgets/tier_avatar_ring.dart';
|
||||
import 'learn_more_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import '../core/api/api_endpoints.dart';
|
||||
import '../core/api/api_client.dart';
|
||||
import '../core/app_decoration.dart';
|
||||
import '../core/constants.dart';
|
||||
import '../widgets/landscape_section_header.dart';
|
||||
import '../features/share/share_rank_card.dart';
|
||||
import '../features/share/share_card_generator.dart';
|
||||
import '../core/analytics/posthog_service.dart';
|
||||
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
@@ -35,6 +38,7 @@ class ProfileScreen extends StatefulWidget {
|
||||
|
||||
class _ProfileScreenState extends State<ProfileScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final ApiClient _apiClient = ApiClient();
|
||||
String _username = '';
|
||||
String _email = 'not provided';
|
||||
String _profileImage = '';
|
||||
@@ -44,6 +48,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
DateTime? _districtChangedAt;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
// Share rank
|
||||
bool _sharingRank = false;
|
||||
|
||||
// 14 Kerala districts
|
||||
static const List<String> _districts = [
|
||||
'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha',
|
||||
@@ -180,6 +187,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
_profileImage =
|
||||
prefs.getString(profileImageKey) ?? prefs.getString('profileImage') ?? '';
|
||||
|
||||
// Always fetch fresh data from server (fire-and-forget)
|
||||
// Ensures profile photo, district, and cooldown are always up-to-date
|
||||
_fetchAndCacheStatusData(prefs, profileImageKey);
|
||||
|
||||
// AUTH-003/PROF-001: Eventify ID
|
||||
_eventifyId = prefs.getString('eventify_id');
|
||||
|
||||
@@ -199,6 +210,48 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
/// Fetches fresh data from /user/status/ on every profile open.
|
||||
/// Updates profile photo, district, and district_changed_at.
|
||||
Future<void> _fetchAndCacheStatusData(SharedPreferences prefs, String profileImageKey) async {
|
||||
try {
|
||||
final res = await _apiClient.post(ApiEndpoints.status);
|
||||
|
||||
// Profile photo
|
||||
final raw = res['profile_photo']?.toString() ?? '';
|
||||
if (raw.isNotEmpty && !raw.contains('default.png')) {
|
||||
final url = raw.startsWith('http') ? raw : 'https://em.eventifyplus.com$raw';
|
||||
await prefs.setString(profileImageKey, url);
|
||||
await prefs.setString('profileImage', url);
|
||||
if (mounted) setState(() => _profileImage = url);
|
||||
}
|
||||
|
||||
// Eventify ID
|
||||
final eventifyId = res['eventify_id']?.toString() ?? '';
|
||||
if (eventifyId.isNotEmpty) {
|
||||
await prefs.setString('eventify_id', eventifyId);
|
||||
if (mounted) setState(() => _eventifyId = eventifyId);
|
||||
}
|
||||
|
||||
// District + cooldown timestamp
|
||||
final districtFromServer = res['district']?.toString() ?? '';
|
||||
final changedAtStr = res['district_changed_at']?.toString() ?? '';
|
||||
if (districtFromServer.isNotEmpty) {
|
||||
await prefs.setString('district', districtFromServer);
|
||||
}
|
||||
if (changedAtStr.isNotEmpty) {
|
||||
await prefs.setString('district_changed_at', changedAtStr);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (districtFromServer.isNotEmpty) _district = districtFromServer;
|
||||
if (changedAtStr.isNotEmpty) _districtChangedAt = DateTime.tryParse(changedAtStr);
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
// Non-critical — silently ignore if status call fails
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadEventsForProfile([SharedPreferences? prefs]) async {
|
||||
_ongoingEvents = [];
|
||||
|
||||
@@ -341,26 +394,24 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final token = prefs.getString('access_token') ?? '';
|
||||
final response = await http.patch(
|
||||
Uri.parse('${ApiEndpoints.baseUrl}/user/update-profile/'),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode({'district': district}),
|
||||
final res = await _apiClient.post(
|
||||
ApiEndpoints.updateProfile,
|
||||
body: {'district': district},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final now = DateTime.now();
|
||||
await prefs.setString('district', district);
|
||||
await prefs.setString('district_changed_at', now.toIso8601String());
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_district = district;
|
||||
_districtChangedAt = now;
|
||||
});
|
||||
}
|
||||
final savedDistrict = res['district']?.toString() ?? district;
|
||||
final changedAtStr = res['district_changed_at']?.toString() ?? '';
|
||||
final now = DateTime.now();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('district', savedDistrict);
|
||||
final changedAt = changedAtStr.isNotEmpty
|
||||
? (DateTime.tryParse(changedAtStr) ?? now)
|
||||
: now;
|
||||
await prefs.setString('district_changed_at', changedAt.toIso8601String());
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_district = savedDistrict;
|
||||
_districtChangedAt = changedAt;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('District update error: $e');
|
||||
@@ -371,6 +422,198 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the share card via dart:ui Canvas and shares as PNG + caption.
|
||||
Future<void> _shareRank(String tier, int lifetimeEp) async {
|
||||
if (_sharingRank) return;
|
||||
setState(() => _sharingRank = true);
|
||||
try {
|
||||
final imageUrl = _profileImage.isNotEmpty &&
|
||||
!_profileImage.contains('default.png')
|
||||
? (_profileImage.startsWith('http')
|
||||
? _profileImage
|
||||
: 'https://em.eventifyplus.com$_profileImage')
|
||||
: null;
|
||||
|
||||
final gam = context.read<GamificationProvider>();
|
||||
final p = gam.profile;
|
||||
final stats = gam.currentUserStats;
|
||||
|
||||
final resolvedTier = stats?.level ?? _userTier ?? tier;
|
||||
final resolvedEp = p?.lifetimeEp ?? lifetimeEp;
|
||||
|
||||
final bytes = await generateShareCardPng(
|
||||
username: _username,
|
||||
tier: resolvedTier,
|
||||
lifetimeEp: resolvedEp,
|
||||
currentEp: p?.currentEp ?? 0,
|
||||
rewardPoints: p?.currentRp ?? 0,
|
||||
eventifyId: _eventifyId,
|
||||
district: _district,
|
||||
imageUrl: imageUrl,
|
||||
);
|
||||
|
||||
final shareText =
|
||||
"I'm a ${resolvedTier.toUpperCase()} Explorer on Eventify Plus! "
|
||||
"${formatEp(resolvedEp)} EP earned. "
|
||||
"Let's connect on the platform for more.\nhttps://app.eventifyplus.com";
|
||||
|
||||
if (kIsWeb) {
|
||||
try {
|
||||
final xfile = XFile.fromData(
|
||||
bytes,
|
||||
name: 'eventify-rank.png',
|
||||
mimeType: 'image/png',
|
||||
);
|
||||
await Share.shareXFiles(
|
||||
[xfile],
|
||||
subject: 'My Eventify Rank',
|
||||
text: shareText,
|
||||
);
|
||||
} catch (_) {
|
||||
await Clipboard.setData(ClipboardData(text: shareText));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Caption copied! Save the image and paste caption when sharing.'),
|
||||
duration: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = File('${tempDir.path}/eventify_rank.png');
|
||||
await file.writeAsBytes(bytes);
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path)],
|
||||
subject: 'My Eventify Rank',
|
||||
text: shareText,
|
||||
);
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('_shareRank error: $e\n$st');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Share failed: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _sharingRank = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a bottom sheet with the 14 Kerala district pills.
|
||||
/// Used both from the header district row and from the Edit Profile sheet.
|
||||
void _showDistrictPicker() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) {
|
||||
final theme = Theme.of(ctx);
|
||||
return StatefulBuilder(
|
||||
builder: (ctx2, setInner) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(18, 14, 18, 32),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Select Your District',
|
||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Used for leaderboard rankings. Can be changed once every 6 months.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_districtLocked)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.lock_outline, size: 14, color: Colors.amber),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Locked until $_districtNextChange',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.amber, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _districts.map((d) {
|
||||
final isSelected = _district == d;
|
||||
return GestureDetector(
|
||||
onTap: _districtLocked
|
||||
? null
|
||||
: () async {
|
||||
setInner(() {});
|
||||
Navigator.of(ctx2).pop();
|
||||
await _updateDistrict(d);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0xFF3B82F6)
|
||||
: (_districtLocked
|
||||
? theme.dividerColor.withOpacity(0.5)
|
||||
: theme.cardColor),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFF3B82F6)
|
||||
: theme.dividerColor,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
d,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: (_districtLocked
|
||||
? theme.hintColor
|
||||
: theme.textTheme.bodyMedium?.color),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _enterAssetPathDialog() async {
|
||||
final ctl = TextEditingController(text: _profileImage);
|
||||
final result = await showDialog<String?>(
|
||||
@@ -520,83 +763,37 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// PROF-002: District picker
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'District',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
// PROF-002: District — tap to open dedicated picker sheet
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_showDistrictPicker();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// AUTH-005: Cooldown lock indicator
|
||||
if (_districtLocked)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.lock_outline,
|
||||
size: 14, color: Colors.amber),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'District locked until $_districtNextChange',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Colors.amber),
|
||||
const Icon(Icons.location_on_outlined, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_district ?? 'Tap to set district',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
_districtLocked ? Icons.lock_outline : Icons.chevron_right,
|
||||
size: 18,
|
||||
color: _districtLocked ? Colors.amber : theme.hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// District pill grid
|
||||
StatefulBuilder(
|
||||
builder: (ctx2, setInner) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _districts.map((d) {
|
||||
final isSelected = _district == d;
|
||||
return GestureDetector(
|
||||
onTap: _districtLocked
|
||||
? null
|
||||
: () async {
|
||||
setInner(() {});
|
||||
await _updateDistrict(d);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0xFF3B82F6)
|
||||
: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFF3B82F6)
|
||||
: theme.dividerColor,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
d,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: theme.textTheme.bodyMedium?.color,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
@@ -616,11 +813,13 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
|
||||
Widget _buildProfileAvatar({double size = 96}) {
|
||||
final path = _profileImage.trim();
|
||||
// Resolve a network-compatible URL: http URLs pass through directly,
|
||||
// file paths and assets fall back to null so DiceBear is used.
|
||||
String? imageUrl;
|
||||
if (path.startsWith('http')) {
|
||||
imageUrl = path;
|
||||
if (path.isNotEmpty && !path.contains('default.png')) {
|
||||
if (path.startsWith('http')) {
|
||||
imageUrl = path;
|
||||
} else if (path.startsWith('/media/')) {
|
||||
imageUrl = 'https://em.eventifyplus.com$path';
|
||||
}
|
||||
}
|
||||
return TierAvatarRing(
|
||||
username: _username.isNotEmpty ? _username : _email,
|
||||
@@ -876,17 +1075,26 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Location (District)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
p?.district ?? _district ?? 'No district selected',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
// Location (District) — tappable to open picker
|
||||
GestureDetector(
|
||||
onTap: _showDistrictPicker,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_district ?? 'Tap to set district',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
_districtLocked ? Icons.lock_outline : Icons.edit_outlined,
|
||||
color: Colors.white70,
|
||||
size: 14,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_districtNextChange.isNotEmpty)
|
||||
Padding(
|
||||
@@ -935,23 +1143,11 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildHeaderButton(
|
||||
label: 'Share Rank',
|
||||
icon: Icons.share_outlined,
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: ShareRankCard(
|
||||
username: _username,
|
||||
tier: stats?.level ?? _userTier ?? '',
|
||||
rank: stats?.rank ?? 0,
|
||||
ep: p?.currentEp ?? 0,
|
||||
rewardPoints: p?.currentRp ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
label: _sharingRank ? 'Generating...' : 'Share Rank',
|
||||
icon: _sharingRank ? Icons.hourglass_top_outlined : Icons.share_outlined,
|
||||
onTap: _sharingRank
|
||||
? () {}
|
||||
: () => _shareRank(tier, p?.lifetimeEp ?? 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1295,28 +1491,22 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
// Share rank card button
|
||||
Consumer<GamificationProvider>(
|
||||
builder: (context, gam, _) => GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: ShareRankCard(
|
||||
username: _username,
|
||||
tier: gam.currentUserStats?.level ?? _userTier ?? '',
|
||||
rank: gam.currentUserStats?.rank ?? 0,
|
||||
ep: gam.profile?.currentEp ?? 0,
|
||||
rewardPoints: gam.profile?.currentRp ?? 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: _sharingRank
|
||||
? null
|
||||
: () => _shareRank(
|
||||
gam.currentUserStats?.level ?? _userTier ?? 'Bronze',
|
||||
gam.profile?.lifetimeEp ?? 0,
|
||||
),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white24,
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: const Icon(Icons.share_outlined, color: Colors.white),
|
||||
child: Icon(
|
||||
_sharingRank ? Icons.hourglass_top_outlined : Icons.share_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1934,35 +2124,37 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
Widget _buildDesktopLayout(BuildContext context, ThemeData theme) {
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
body: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Full-width profile header + card (reuse existing widgets)
|
||||
Stack(
|
||||
body: Consumer<GamificationProvider>(
|
||||
builder: (context, gam, _) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildGradientHeader(context, 200),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 130),
|
||||
child: _buildProfileCard(context),
|
||||
// Full-width profile header + card (reuse existing widgets)
|
||||
Stack(
|
||||
children: [
|
||||
_buildGradientHeader(context, 200),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 130),
|
||||
child: _buildProfileCard(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Ongoing Events (only if non-empty)
|
||||
if (_ongoingEvents.isNotEmpty)
|
||||
_buildDesktopEventSection(
|
||||
context,
|
||||
title: 'Ongoing Events',
|
||||
events: _ongoingEvents,
|
||||
faded: false,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Ongoing Events (only if non-empty)
|
||||
if (_ongoingEvents.isNotEmpty)
|
||||
_buildDesktopEventSection(
|
||||
context,
|
||||
title: 'Ongoing Events',
|
||||
events: _ongoingEvents,
|
||||
faded: false,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2237,24 +2429,16 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
|
||||
// ── MOBILE layout ─────────────────────────────────────────────────────
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF9FAFB), // Softer light bg
|
||||
backgroundColor: const Color(0xFFF9FAFB),
|
||||
body: Consumer<GamificationProvider>(
|
||||
builder: (context, gam, _) {
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _buildModernHeader(context, theme),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildModernStatCards(gam, theme),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildTierProgressCard(gam, theme),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildContributedEventsSection(gam, theme),
|
||||
),
|
||||
SliverToBoxAdapter(child: _buildModernHeader(context, theme)),
|
||||
SliverToBoxAdapter(child: _buildModernStatCards(gam, theme)),
|
||||
SliverToBoxAdapter(child: _buildTierProgressCard(gam, theme)),
|
||||
SliverToBoxAdapter(child: _buildContributedEventsSection(gam, theme)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user