...
This commit is contained in:
@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import '../../../core/utils/error_utils.dart';
|
import '../../../core/utils/error_utils.dart';
|
||||||
import '../models/gamification_models.dart';
|
import '../models/gamification_models.dart';
|
||||||
import '../services/gamification_service.dart';
|
import '../services/gamification_service.dart';
|
||||||
|
import '../../events/services/events_service.dart';
|
||||||
|
import '../../events/models/event_models.dart';
|
||||||
|
|
||||||
class GamificationProvider extends ChangeNotifier {
|
class GamificationProvider extends ChangeNotifier {
|
||||||
final GamificationService _service = GamificationService();
|
final GamificationService _service = GamificationService();
|
||||||
@@ -16,6 +18,7 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
List<SubmissionModel> submissions = [];
|
List<SubmissionModel> submissions = [];
|
||||||
CurrentUserStats? currentUserStats;
|
CurrentUserStats? currentUserStats;
|
||||||
int totalParticipants = 0;
|
int totalParticipants = 0;
|
||||||
|
List<String> eventCategories = [];
|
||||||
|
|
||||||
// Leaderboard filters — matches web version
|
// Leaderboard filters — matches web version
|
||||||
String leaderboardDistrict = 'Overall Kerala';
|
String leaderboardDistrict = 'Overall Kerala';
|
||||||
@@ -44,26 +47,53 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final results = await Future.wait([
|
final results = await Future.wait([
|
||||||
_service.getDashboard(),
|
_service.getDashboard().catchError((e) {
|
||||||
_service.getShopItems(),
|
debugPrint('Dashboard error: $e');
|
||||||
_service.getAchievements(),
|
return const DashboardResponse(profile: UserGamificationProfile(userId: '', lifetimeEp: 0, currentEp: 0, currentRp: 0, tier: ContributorTier.BRONZE));
|
||||||
|
}),
|
||||||
|
_service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod).catchError((e) {
|
||||||
|
debugPrint('Leaderboard error: $e');
|
||||||
|
return const LeaderboardResponse(entries: []);
|
||||||
|
}),
|
||||||
|
_service.getShopItems().catchError((e) {
|
||||||
|
debugPrint('Shop error: $e');
|
||||||
|
return <ShopItem>[];
|
||||||
|
}),
|
||||||
|
_service.getAchievements().catchError((e) {
|
||||||
|
debugPrint('Achievements error: $e');
|
||||||
|
return <AchievementBadge>[];
|
||||||
|
}),
|
||||||
|
EventsService().getEventTypes().catchError((e) {
|
||||||
|
debugPrint('EventTypes error: $e');
|
||||||
|
return <EventTypeModel>[];
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final dashboard = results[0] as DashboardResponse;
|
final dashboard = results[0] as DashboardResponse;
|
||||||
profile = dashboard.profile;
|
profile = dashboard.profile;
|
||||||
submissions = dashboard.submissions;
|
submissions = dashboard.submissions;
|
||||||
|
|
||||||
shopItems = results[1] as List<ShopItem>;
|
final lbResponse = results[1] as LeaderboardResponse;
|
||||||
|
leaderboard = _filterAndReRank(lbResponse.entries, leaderboardDistrict, leaderboardTimePeriod);
|
||||||
|
currentUserStats = lbResponse.currentUser;
|
||||||
|
totalParticipants = lbResponse.totalParticipants;
|
||||||
|
|
||||||
|
shopItems = results[2] as List<ShopItem>;
|
||||||
|
|
||||||
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
|
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
|
||||||
final dashAchievements = dashboard.achievements;
|
final dashAchievements = dashboard.achievements;
|
||||||
final fetchedAchievements = results[2] as List<AchievementBadge>;
|
final fetchedAchievements = results[3] as List<AchievementBadge>;
|
||||||
|
|
||||||
if (dashAchievements.isNotEmpty) {
|
if (dashAchievements.isNotEmpty) {
|
||||||
achievements = dashAchievements;
|
achievements = dashAchievements;
|
||||||
} else if (fetchedAchievements.isNotEmpty) {
|
} else if (fetchedAchievements.isNotEmpty) {
|
||||||
achievements = fetchedAchievements;
|
achievements = fetchedAchievements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final eventTypes = results[4] as List<EventTypeModel>;
|
||||||
|
if (eventTypes.isNotEmpty) {
|
||||||
|
eventCategories = eventTypes.map((e) => e.name).toList();
|
||||||
|
}
|
||||||
// Otherwise, keep current defaults
|
// Otherwise, keep current defaults
|
||||||
|
|
||||||
_lastLoadTime = DateTime.now();
|
_lastLoadTime = DateTime.now();
|
||||||
@@ -107,7 +137,7 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
|
final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
|
||||||
leaderboard = response.entries;
|
leaderboard = _filterAndReRank(response.entries, district, leaderboardTimePeriod);
|
||||||
currentUserStats = response.currentUser;
|
currentUserStats = response.currentUser;
|
||||||
totalParticipants = response.totalParticipants;
|
totalParticipants = response.totalParticipants;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -128,7 +158,7 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
|
final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
|
||||||
leaderboard = response.entries;
|
leaderboard = _filterAndReRank(response.entries, leaderboardDistrict, period);
|
||||||
currentUserStats = response.currentUser;
|
currentUserStats = response.currentUser;
|
||||||
totalParticipants = response.totalParticipants;
|
totalParticipants = response.totalParticipants;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -182,4 +212,41 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
Future<void> submitContribution(Map<String, dynamic> data) async {
|
Future<void> submitContribution(Map<String, dynamic> data) async {
|
||||||
await _service.submitContribution(data);
|
await _service.submitContribution(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: Filter by district and re-rank results locally.
|
||||||
|
// This is a fallback in case the backend returns a global list for a district-specific query.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
List<LeaderboardEntry> _filterAndReRank(List<LeaderboardEntry> entries, String district, String period) {
|
||||||
|
if (entries.isEmpty) return [];
|
||||||
|
|
||||||
|
List<LeaderboardEntry> result = entries;
|
||||||
|
if (district != 'Overall Kerala') {
|
||||||
|
// Case-insensitive filtering to be robust
|
||||||
|
result = entries.where((e) => e.district?.toLowerCase() == district.toLowerCase()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort based on period
|
||||||
|
if (period == 'this_month') {
|
||||||
|
result.sort((a, b) => b.monthlyPoints.compareTo(a.monthlyPoints));
|
||||||
|
} else {
|
||||||
|
result.sort((a, b) => b.lifetimeEp.compareTo(a.lifetimeEp));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign new ranks based on local sort order
|
||||||
|
return List.generate(result.length, (i) {
|
||||||
|
final e = result[i];
|
||||||
|
return LeaderboardEntry(
|
||||||
|
rank: i + 1,
|
||||||
|
username: e.username,
|
||||||
|
avatarUrl: e.avatarUrl,
|
||||||
|
lifetimeEp: e.lifetimeEp,
|
||||||
|
monthlyPoints: e.monthlyPoints,
|
||||||
|
tier: e.tier,
|
||||||
|
eventsCount: e.eventsCount,
|
||||||
|
isCurrentUser: e.isCurrentUser,
|
||||||
|
district: e.district,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const _districts = [
|
|||||||
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 'Other',
|
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 'Other',
|
||||||
];
|
];
|
||||||
|
|
||||||
const _categories = [
|
const _categories_fallback = [
|
||||||
'Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community',
|
'Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community',
|
||||||
'Dance', 'Film', 'Business', 'Health', 'Education', 'Other',
|
'Dance', 'Film', 'Business', 'Health', 'Education', 'Other',
|
||||||
];
|
];
|
||||||
@@ -88,7 +88,7 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
|
|
||||||
DateTime? _selectedDate;
|
DateTime? _selectedDate;
|
||||||
TimeOfDay? _selectedTime;
|
TimeOfDay? _selectedTime;
|
||||||
String _selectedCategory = _categories.first;
|
String _selectedCategory = 'Music';
|
||||||
String _selectedDistrict = _districts.first;
|
String _selectedDistrict = _districts.first;
|
||||||
List<XFile> _images = [];
|
List<XFile> _images = [];
|
||||||
bool _submitting = false;
|
bool _submitting = false;
|
||||||
@@ -135,6 +135,13 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
body: Center(child: BouncingLoader(color: Colors.white)),
|
body: Center(child: BouncingLoader(color: Colors.white)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Sync _selectedCategory with provider data if it's missing from current list
|
||||||
|
if (provider.eventCategories.isNotEmpty && !provider.eventCategories.contains(_selectedCategory)) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) setState(() => _selectedCategory = provider.eventCategories.first);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white, // Changed from _blue
|
backgroundColor: Colors.white, // Changed from _blue
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
@@ -287,9 +294,36 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.emoji_events_outlined, size: 48, color: Colors.grey.shade300),
|
Container(
|
||||||
const SizedBox(height: 12),
|
padding: const EdgeInsets.all(24),
|
||||||
Text('No rankings available for this area.', style: TextStyle(color: Colors.grey.shade400)),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.emoji_events_rounded, size: 72, color: Colors.amber),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'No Contributor Yet',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: _darkText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: Text(
|
||||||
|
'No contributors in $currentDistrict yet. Be the first to join the ranks!',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: _subText,
|
||||||
|
fontSize: 15,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1181,7 +1215,13 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
children: [
|
children: [
|
||||||
_inputLabel('Category', required: true),
|
_inputLabel('Category', required: true),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_dropdown(_selectedCategory, _categories, (v) => setState(() => _selectedCategory = v!)),
|
if (provider.eventCategories.isEmpty)
|
||||||
|
const SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
_dropdown(_selectedCategory, provider.eventCategories, (v) => setState(() => _selectedCategory = v!)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1799,7 +1839,7 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
_mapsLinkCtl.clear();
|
_mapsLinkCtl.clear();
|
||||||
_selectedDate = null;
|
_selectedDate = null;
|
||||||
_selectedTime = null;
|
_selectedTime = null;
|
||||||
_selectedCategory = _categories.first;
|
_selectedCategory = _categories_fallback.first;
|
||||||
_selectedDistrict = _districts.first;
|
_selectedDistrict = _districts.first;
|
||||||
_images.clear();
|
_images.clear();
|
||||||
_coordMessage = null;
|
_coordMessage = null;
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
|
|||||||
Future<void> _shareEvent() async {
|
Future<void> _shareEvent() async {
|
||||||
final title = _event?.title ?? _event?.name ?? 'Check out this event';
|
final title = _event?.title ?? _event?.name ?? 'Check out this event';
|
||||||
final url =
|
final url =
|
||||||
'https://uat.eventifyplus.com/events/${widget.eventId}';
|
'https://app.eventifyplus.com/event/${widget.eventId}';
|
||||||
await Share.share('$title\n$url', subject: title);
|
await Share.share('$title\n$url', subject: title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<EventModel> _ongoingEvents = [];
|
List<EventModel> _ongoingEvents = [];
|
||||||
List<EventModel> _upcomingEvents = [];
|
|
||||||
List<EventModel> _pastEvents = [];
|
|
||||||
|
|
||||||
bool _loadingEvents = true;
|
bool _loadingEvents = true;
|
||||||
|
|
||||||
@@ -202,12 +200,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadEventsForProfile([SharedPreferences? prefs]) async {
|
Future<void> _loadEventsForProfile([SharedPreferences? prefs]) async {
|
||||||
setState(() {
|
|
||||||
_loadingEvents = true;
|
|
||||||
_ongoingEvents = [];
|
_ongoingEvents = [];
|
||||||
_upcomingEvents = [];
|
|
||||||
_pastEvents = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
prefs ??= await SharedPreferences.getInstance();
|
prefs ??= await SharedPreferences.getInstance();
|
||||||
final pincode = prefs.getString('pincode') ?? 'all';
|
final pincode = prefs.getString('pincode') ?? 'all';
|
||||||
@@ -218,8 +211,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
final ongoing = <EventModel>[];
|
final ongoing = <EventModel>[];
|
||||||
final upcoming = <EventModel>[];
|
|
||||||
final past = <EventModel>[];
|
|
||||||
|
|
||||||
DateTime? tryParseDate(String? s) {
|
DateTime? tryParseDate(String? s) {
|
||||||
if (s == null) return null;
|
if (s == null) return null;
|
||||||
@@ -235,35 +226,21 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
final parsedEnd = tryParseDate(e.endDate);
|
final parsedEnd = tryParseDate(e.endDate);
|
||||||
|
|
||||||
if (parsedStart == null) {
|
if (parsedStart == null) {
|
||||||
upcoming.add(e);
|
// treat as ongoing or handle differently
|
||||||
} else if (parsedStart.isAtSameMomentAs(today) ||
|
} else if (parsedStart.isAtSameMomentAs(today) ||
|
||||||
(parsedStart.isBefore(today) &&
|
(parsedStart.isBefore(today) &&
|
||||||
parsedEnd != null &&
|
parsedEnd != null &&
|
||||||
!parsedEnd.isBefore(today))) {
|
!parsedEnd.isBefore(today))) {
|
||||||
ongoing.add(e);
|
ongoing.add(e);
|
||||||
} else if (parsedStart.isBefore(today)) {
|
} else if (parsedStart.isBefore(today)) {
|
||||||
past.add(e);
|
// ignore past
|
||||||
} else {
|
|
||||||
upcoming.add(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
upcoming.sort((a, b) {
|
|
||||||
final da = tryParseDate(a.startDate) ?? DateTime(9999);
|
|
||||||
final db = tryParseDate(b.startDate) ?? DateTime(9999);
|
|
||||||
return da.compareTo(db);
|
|
||||||
});
|
|
||||||
past.sort((a, b) {
|
|
||||||
final da = tryParseDate(a.startDate) ?? DateTime(0);
|
|
||||||
final db = tryParseDate(b.startDate) ?? DateTime(0);
|
|
||||||
return db.compareTo(da);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_ongoingEvents = ongoing;
|
_ongoingEvents = ongoing;
|
||||||
_upcomingEvents = upcoming;
|
|
||||||
_pastEvents = past;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -787,8 +764,516 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
// NEW UI WIDGETS — matching web profile layout
|
// NEW UI WIDGETS — matching web profile layout
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// MODERN UI WIDGETS
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
|
Widget _buildModernHeader(BuildContext context, ThemeData theme) {
|
||||||
|
return Consumer<GamificationProvider>(
|
||||||
|
builder: (context, gam, _) {
|
||||||
|
final stats = gam.currentUserStats;
|
||||||
|
final p = gam.profile;
|
||||||
|
final tier = stats?.level ?? _userTier ?? 'Bronze';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.only(bottom: 30),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Color(0xFF2563EB), Color(0xFF3B82F6)],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(40),
|
||||||
|
bottomRight: Radius.circular(40),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Top Bar
|
||||||
|
SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white24,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.settings, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Profile Avatar
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.5), width: 2),
|
||||||
|
),
|
||||||
|
child: _buildProfileAvatar(size: 100),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Username
|
||||||
|
Text(
|
||||||
|
_username,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Eventify ID Badge
|
||||||
|
if (_eventifyId != null)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: _eventifyId!));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('ID copied to clipboard')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: Colors.white.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.copy, size: 14, color: Colors.white70),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_eventifyId!,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
_district ?? 'No district selected',
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_districtNextChange.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
'Next change: $_districtNextChange',
|
||||||
|
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Tier Badge
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFD1A9), // Matching "Bronze" color from screenshot
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.shield_outlined, size: 18, color: Color(0xFF92400E)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
tier.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF92400E),
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildHeaderButton(
|
||||||
|
label: 'Edit Profile',
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
onTap: _openEditDialog,
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeaderButton({required String label, required IconData icon, required VoidCallback onTap}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.3)),
|
||||||
|
color: Colors.white.withValues(alpha: 0.05),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.white, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildModernStatCards(GamificationProvider gam, ThemeData theme) {
|
||||||
|
final p = gam.profile;
|
||||||
|
if (p == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildStatRowCard('Lifetime EP', '${p.lifetimeEp}', Icons.bolt, const Color(0xFFEFF6FF), const Color(0xFF3B82F6)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildStatRowCard('Liquid EP', '${p.currentEp}', Icons.hexagon_outlined, const Color(0xFFF0FDF4), const Color(0xFF22C55E)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildStatRowCard('Reward Points', '${p.currentRp}', Icons.card_giftcard, const Color(0xFFFFFBEB), const Color(0xFFF59E0B)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatRowCard(String label, String value, IconData icon, Color bgColor, Color iconColor) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: Colors.grey.shade100),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 10, offset: const Offset(0, 4)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: iconColor, size: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey.shade500, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTierProgressCard(GamificationProvider gam, ThemeData theme) {
|
||||||
|
final ep = gam.profile?.lifetimeEp ?? 0;
|
||||||
|
final currentTier = tierFromEp(ep);
|
||||||
|
final nextThreshold = nextTierThreshold(currentTier);
|
||||||
|
final startEp = tierStartEp(currentTier);
|
||||||
|
|
||||||
|
double progress = 0.0;
|
||||||
|
String nextInfo = '';
|
||||||
|
if (nextThreshold != null) {
|
||||||
|
final needed = nextThreshold - ep;
|
||||||
|
final range = nextThreshold - startEp;
|
||||||
|
progress = ((ep - startEp) / range).clamp(0.0, 1.0);
|
||||||
|
nextInfo = '$needed EP to ${tierLabel(ContributorTier.values[currentTier.index + 1]).toUpperCase()}';
|
||||||
|
} else {
|
||||||
|
progress = 1.0;
|
||||||
|
nextInfo = 'MAX TIER REACHED';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 18),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(color: Colors.grey.shade100),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
tierLabel(currentTier).toUpperCase(),
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
nextInfo,
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade500, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
minHeight: 10,
|
||||||
|
backgroundColor: const Color(0xFFF3F4F6),
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2563EB)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
'$ep EP total',
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade400, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContributedEventsSection(GamificationProvider gam, ThemeData theme) {
|
||||||
|
final submissions = gam.submissions;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 32, 20, 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Contributed Events',
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF3F4F6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${submissions.length}',
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFF6B7280)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (submissions.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
child: Text('No contributions yet. Start contributing to earn EP!'),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||||
|
itemCount: submissions.length,
|
||||||
|
itemBuilder: (ctx, i) => _buildSubmissionCard(submissions[i], theme),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubmissionCard(SubmissionModel sub, ThemeData theme) {
|
||||||
|
Color statusColor;
|
||||||
|
Color statusBgColor;
|
||||||
|
String statusLabel = sub.status.toUpperCase();
|
||||||
|
|
||||||
|
switch (sub.status.toUpperCase()) {
|
||||||
|
case 'APPROVED':
|
||||||
|
statusColor = const Color(0xFF059669);
|
||||||
|
statusBgColor = const Color(0xFFD1FAE5);
|
||||||
|
break;
|
||||||
|
case 'REJECTED':
|
||||||
|
statusColor = const Color(0xFFDC2626);
|
||||||
|
statusBgColor = const Color(0xFFFEE2E2);
|
||||||
|
break;
|
||||||
|
default: // PENDING
|
||||||
|
statusColor = const Color(0xFFD97706);
|
||||||
|
statusBgColor = const Color(0xFFFEF3C7);
|
||||||
|
statusLabel = 'PENDING';
|
||||||
|
}
|
||||||
|
|
||||||
|
final dateStr = '${sub.createdAt.day} ${_getMonth(sub.createdAt.month)} ${sub.createdAt.year}';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: Colors.grey.shade100),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.01), blurRadius: 10, offset: const Offset(0, 4)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
sub.eventName,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: Color(0xFF1F2937)),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusBgColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.access_time_filled, size: 14, color: statusColor),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
statusLabel,
|
||||||
|
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: statusColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.calendar_today_outlined, size: 14, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(dateStr, style: TextStyle(color: Colors.grey.shade500, fontSize: 13)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Icon(Icons.location_on_outlined, size: 14, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
sub.district ?? 'Unknown',
|
||||||
|
style: TextStyle(color: Colors.grey.shade500, fontSize: 13),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (sub.status.toUpperCase() == 'APPROVED' && sub.epAwarded > 0)
|
||||||
|
Text(
|
||||||
|
'+${sub.epAwarded} EP',
|
||||||
|
style: const TextStyle(color: Color(0xFF059669), fontWeight: FontWeight.w800, fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (sub.category.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF3F4F6),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
sub.category,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Color(0xFF6B7280), fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getMonth(int m) {
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
return months[m - 1];
|
||||||
|
}
|
||||||
|
|
||||||
// ───────── Gradient Header ─────────
|
// ───────── Gradient Header ─────────
|
||||||
|
|
||||||
|
|
||||||
Widget _buildGradientHeader(BuildContext context, double height) {
|
Widget _buildGradientHeader(BuildContext context, double height) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -1224,43 +1709,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
_ongoingEvents.map((e) => _eventListTileFromModel(e)).toList()),
|
_ongoingEvents.map((e) => _eventListTileFromModel(e)).toList()),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Upcoming Events
|
|
||||||
sectionHeading('Upcoming Events'),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
if (_loadingEvents)
|
|
||||||
const SizedBox.shrink()
|
|
||||||
else if (_upcomingEvents.isEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: Text('No upcoming events',
|
|
||||||
style: theme.textTheme.bodyMedium
|
|
||||||
?.copyWith(color: theme.hintColor)),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Column(
|
|
||||||
children: _upcomingEvents
|
|
||||||
.map((e) => _eventListTileFromModel(e))
|
|
||||||
.toList()),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Past Events
|
|
||||||
sectionHeading('Past Events'),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
if (_loadingEvents)
|
|
||||||
const SizedBox.shrink()
|
|
||||||
else if (_pastEvents.isEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: Text('No past events',
|
|
||||||
style: theme.textTheme.bodyMedium
|
|
||||||
?.copyWith(color: theme.hintColor)),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Column(
|
|
||||||
children: _pastEvents
|
|
||||||
.map((e) => _eventListTileFromModel(e, faded: true))
|
|
||||||
.toList()),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1438,7 +1886,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 3,
|
length: 1,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@@ -1461,8 +1909,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(text: 'Ongoing'),
|
Tab(text: 'Ongoing'),
|
||||||
Tab(text: 'Upcoming'),
|
|
||||||
Tab(text: 'Past'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1471,8 +1917,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_eventList(_ongoingEvents),
|
_eventList(_ongoingEvents),
|
||||||
_eventList(_upcomingEvents),
|
|
||||||
_eventList(_pastEvents, faded: true),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1515,24 +1959,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
faded: false,
|
faded: false,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Upcoming Events
|
|
||||||
_buildDesktopEventSection(
|
|
||||||
context,
|
|
||||||
title: 'Upcoming Events',
|
|
||||||
events: _upcomingEvents,
|
|
||||||
faded: false,
|
|
||||||
emptyMessage: 'No upcoming events',
|
|
||||||
),
|
|
||||||
|
|
||||||
// Past Events
|
|
||||||
_buildDesktopEventSection(
|
|
||||||
context,
|
|
||||||
title: 'Past Events',
|
|
||||||
events: _pastEvents,
|
|
||||||
faded: true,
|
|
||||||
emptyMessage: 'No past events',
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1801,19 +2227,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
const double headerHeight = 200.0;
|
|
||||||
const double cardTopOffset = 130.0;
|
|
||||||
final width = MediaQuery.of(context).size.width;
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
Widget sectionTitle(String text) => Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(18, 16, 18, 12),
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
style: theme.textTheme.titleMedium
|
|
||||||
?.copyWith(fontWeight: FontWeight.w600, fontSize: 18),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── DESKTOP / LANDSCAPE layout ─────────────────────────────────────────
|
// ── DESKTOP / LANDSCAPE layout ─────────────────────────────────────────
|
||||||
if (width >= AppConstants.desktopBreakpoint) {
|
if (width >= AppConstants.desktopBreakpoint) {
|
||||||
return _buildDesktopLayout(context, theme);
|
return _buildDesktopLayout(context, theme);
|
||||||
@@ -1821,97 +2236,27 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
|
|
||||||
// ── MOBILE layout ─────────────────────────────────────────────────────
|
// ── MOBILE layout ─────────────────────────────────────────────────────
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor: const Color(0xFFF9FAFB), // Softer light bg
|
||||||
// CustomScrollView: only visible event cards are built — no full-tree Column renders
|
body: Consumer<GamificationProvider>(
|
||||||
// SafeArea(bottom: false) — bottom is handled by home screen's floating nav buffer
|
builder: (context, gam, _) {
|
||||||
body: SafeArea(
|
return CustomScrollView(
|
||||||
bottom: false,
|
physics: const BouncingScrollPhysics(),
|
||||||
child: CustomScrollView(
|
slivers: [
|
||||||
physics: const BouncingScrollPhysics(),
|
SliverToBoxAdapter(
|
||||||
slivers: [
|
child: _buildModernHeader(context, theme),
|
||||||
// Header gradient + Profile card overlap (same visual as before)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
_buildGradientHeader(context, headerHeight),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(top: cardTopOffset),
|
|
||||||
child: _buildProfileCard(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// PROF-003: Gamification stat cards
|
|
||||||
SliverToBoxAdapter(child: _buildGamificationCards(theme)),
|
|
||||||
|
|
||||||
// ── Ongoing Events ──
|
|
||||||
if (_ongoingEvents.isNotEmpty) ...[
|
|
||||||
SliverToBoxAdapter(child: sectionTitle('Ongoing Events')),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
||||||
sliver: SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(ctx, i) => _eventListTileFromModel(_ongoingEvents[i]),
|
|
||||||
childCount: _ongoingEvents.length,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
SliverToBoxAdapter(
|
||||||
const SliverToBoxAdapter(child: SizedBox(height: 24)),
|
child: _buildModernStatCards(gam, theme),
|
||||||
],
|
|
||||||
|
|
||||||
// ── Upcoming Events ──
|
|
||||||
SliverToBoxAdapter(child: sectionTitle('Upcoming Events')),
|
|
||||||
if (_loadingEvents)
|
|
||||||
const SliverToBoxAdapter(child: SizedBox.shrink())
|
|
||||||
else if (_upcomingEvents.isEmpty)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
|
||||||
child: Text('No upcoming events',
|
|
||||||
style: theme.textTheme.bodyMedium
|
|
||||||
?.copyWith(color: theme.hintColor)),
|
|
||||||
),
|
),
|
||||||
)
|
SliverToBoxAdapter(
|
||||||
else
|
child: _buildTierProgressCard(gam, theme),
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
||||||
sliver: SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(ctx, i) => _eventListTileFromModel(_upcomingEvents[i]),
|
|
||||||
childCount: _upcomingEvents.length,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
SliverToBoxAdapter(
|
||||||
const SliverToBoxAdapter(child: SizedBox(height: 24)),
|
child: _buildContributedEventsSection(gam, theme),
|
||||||
|
|
||||||
// ── Past Events ──
|
|
||||||
SliverToBoxAdapter(child: sectionTitle('Past Events')),
|
|
||||||
if (_loadingEvents)
|
|
||||||
const SliverToBoxAdapter(child: SizedBox.shrink())
|
|
||||||
else if (_pastEvents.isEmpty)
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
|
||||||
child: Text('No past events',
|
|
||||||
style: theme.textTheme.bodyMedium
|
|
||||||
?.copyWith(color: theme.hintColor)),
|
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
else
|
);
|
||||||
SliverPadding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
|
||||||
sliver: SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(ctx, i) => _eventListTileFromModel(_pastEvents[i], faded: true),
|
|
||||||
childCount: _pastEvents.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user