feat: redesign profile screen to match web app layout

- Add floating white profile card overlapping blue gradient header
- Add cover banner with gradient overlay and edit button
- Add avatar overlapping banner bottom with white border
- Add colorful EXP progress bar (purple→yellow→blue gradient)
- Add stats row (Likes, Posts, Views) with vertical dividers
- Add social icons row with 4 placeholder icons
- Add rainbow gradient accent bar at card bottom
- Split events into Ongoing, Upcoming, and Past sections
- Update event card styling (60px images, 16px radius, refined shadow)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 14:17:37 +05:30
parent d6d8ac6dbf
commit 3816c2c844

View File

@@ -8,8 +8,9 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../features/events/services/events_service.dart';
import '../features/events/models/event_models.dart';
import 'learn_more_screen.dart';
import 'settings_screen.dart'; // <- added import
import 'settings_screen.dart';
import '../core/app_decoration.dart';
import '../core/constants.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({Key? key}) : super(key: key);
@@ -21,40 +22,48 @@ class ProfileScreen extends StatefulWidget {
class _ProfileScreenState extends State<ProfileScreen> {
String _username = '';
String _email = 'not provided';
String _profileImage = ''; // per-account stored path or URL (may be empty)
String _profileImage = '';
final ImagePicker _picker = ImagePicker();
final EventsService _eventsService = EventsService();
// events coming from backend
List<EventModel> _ongoingEvents = [];
List<EventModel> _upcomingEvents = [];
List<EventModel> _pastEvents = [];
bool _loadingEvents = true;
// Gradient used for EXP bar and rainbow bar
static const LinearGradient _expGradient = LinearGradient(
colors: [Color(0xFFA855F7), Color(0xFFEAB308), Color(0xFF3B82F6)],
);
@override
void initState() {
super.initState();
_loadProfile();
}
/// Load profile for the currently signed-in account.
// ───────── Data Loading (unchanged) ─────────
Future<void> _loadProfile() async {
final prefs = await SharedPreferences.getInstance();
// current_email marks the active account (set at login/register)
final currentEmail = prefs.getString('current_email') ?? prefs.getString('email') ?? '';
final currentEmail =
prefs.getString('current_email') ?? prefs.getString('email') ?? '';
_email = currentEmail.isNotEmpty ? currentEmail : 'not provided';
// per-account display name key
final displayKey = currentEmail.isNotEmpty ? 'display_name_$currentEmail' : 'display_name';
final profileImageKey = currentEmail.isNotEmpty ? 'profileImage_$currentEmail' : 'profileImage';
final displayKey =
currentEmail.isNotEmpty ? 'display_name_$currentEmail' : 'display_name';
final profileImageKey =
currentEmail.isNotEmpty ? 'profileImage_$currentEmail' : 'profileImage';
// Prefer per-account display_name, else fallback to email
_username = prefs.getString(displayKey) ?? prefs.getString('display_name') ?? _email;
_profileImage = prefs.getString(profileImageKey) ?? prefs.getString('profileImage') ?? '';
_username = prefs.getString(displayKey) ??
prefs.getString('display_name') ??
_email;
_profileImage =
prefs.getString(profileImageKey) ?? prefs.getString('profileImage') ?? '';
// load events for this account's pincode (or default)
await _loadEventsForProfile(prefs);
if (mounted) setState(() {});
}
@@ -62,6 +71,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
Future<void> _loadEventsForProfile([SharedPreferences? prefs]) async {
setState(() {
_loadingEvents = true;
_ongoingEvents = [];
_upcomingEvents = [];
_pastEvents = [];
});
@@ -73,6 +83,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
final events = await _eventsService.getEventsByPincode(pincode);
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final ongoing = <EventModel>[];
final upcoming = <EventModel>[];
final past = <EventModel>[];
@@ -86,17 +98,22 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
for (final e in events) {
final parsed = tryParseDate(e.startDate);
if (parsed == null) {
final parsedStart = tryParseDate(e.startDate);
final parsedEnd = tryParseDate(e.endDate);
if (parsedStart == null) {
upcoming.add(e);
} else {
if (parsed.isBefore(DateTime(now.year, now.month, now.day))) {
} else if (parsedStart.isAtSameMomentAs(today) ||
(parsedStart.isBefore(today) &&
parsedEnd != null &&
!parsedEnd.isBefore(today))) {
ongoing.add(e);
} else if (parsedStart.isBefore(today)) {
past.add(e);
} else {
upcoming.add(e);
}
}
}
upcoming.sort((a, b) {
final da = tryParseDate(a.startDate) ?? DateTime(9999);
@@ -111,30 +128,34 @@ class _ProfileScreenState extends State<ProfileScreen> {
if (mounted) {
setState(() {
_ongoingEvents = ongoing;
_upcomingEvents = upcoming;
_pastEvents = past;
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to load events: $e')));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to load events: $e')));
}
} finally {
if (mounted) setState(() => _loadingEvents = false);
}
}
Future<void> _saveProfile(String name, String email, String profileImage) async {
Future<void> _saveProfile(
String name, String email, String profileImage) async {
final prefs = await SharedPreferences.getInstance();
final currentEmail = prefs.getString('current_email') ?? prefs.getString('email') ?? email;
final currentEmail =
prefs.getString('current_email') ?? prefs.getString('email') ?? email;
final displayKey = currentEmail.isNotEmpty ? 'display_name_$currentEmail' : 'display_name';
final profileImageKey = currentEmail.isNotEmpty ? 'profileImage_$currentEmail' : 'profileImage';
final displayKey =
currentEmail.isNotEmpty ? 'display_name_$currentEmail' : 'display_name';
final profileImageKey =
currentEmail.isNotEmpty ? 'profileImage_$currentEmail' : 'profileImage';
await prefs.setString(displayKey, name);
await prefs.setString(profileImageKey, profileImage);
// update 'email' canonical pointer and 'current_email' (defensive)
await prefs.setString('email', currentEmail);
await prefs.setString('current_email', currentEmail);
@@ -145,20 +166,25 @@ class _ProfileScreenState extends State<ProfileScreen> {
});
}
// ---------- Image picking / manual path ----------
// ───────── Image picking (unchanged) ─────────
Future<void> _pickFromGallery() async {
try {
final XFile? xfile = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
final XFile? xfile =
await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
if (xfile == null) return;
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Use "Enter Asset Path" on web/desktop or add server URL.')));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content:
Text('Use "Enter Asset Path" on web/desktop or add server URL.')));
return;
}
final String path = xfile.path;
await _saveProfile(_username, _email, path);
} catch (e) {
debugPrint('Image pick error: $e');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e')));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Failed to pick image: $e')));
}
}
@@ -182,13 +208,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
const SizedBox(height: 8),
Text(
'Use an asset path (for bundled images) or an https:// URL (web).',
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
style:
theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(null), child: const Text('Cancel')),
ElevatedButton(onPressed: () => Navigator.of(ctx).pop(ctl.text.trim()), child: const Text('Use')),
TextButton(
onPressed: () => Navigator.of(ctx).pop(null),
child: const Text('Cancel')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(ctl.text.trim()),
child: const Text('Use')),
],
);
},
@@ -218,22 +249,30 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.fromLTRB(18, 14, 18, 18),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
borderRadius:
const BorderRadius.vertical(top: Radius.circular(20)),
),
child: SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
Container(width: 48, height: 6, decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(6))),
Container(
width: 48,
height: 6,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: BorderRadius.circular(6))),
const SizedBox(height: 12),
Row(
children: [
Text('Edit profile', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
Text('Edit profile',
style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold)),
const Spacer(),
IconButton(
icon: const Icon(Icons.photo_camera),
onPressed: () async {
Navigator.of(context).pop(); // close sheet then launch picker to avoid nested modal issues
Navigator.of(context).pop();
await _pickFromGallery();
},
),
@@ -247,23 +286,39 @@ class _ProfileScreenState extends State<ProfileScreen> {
],
),
const SizedBox(height: 8),
TextField(controller: nameCtl, decoration: InputDecoration(labelText: 'Name', filled: true, fillColor: theme.cardColor, border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
TextField(
controller: nameCtl,
decoration: InputDecoration(
labelText: 'Name',
filled: true,
fillColor: theme.cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8)))),
const SizedBox(height: 8),
TextField(controller: emailCtl, decoration: InputDecoration(labelText: 'Email', filled: true, fillColor: theme.cardColor, border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
TextField(
controller: emailCtl,
decoration: InputDecoration(
labelText: 'Email',
filled: true,
fillColor: theme.cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8)))),
const SizedBox(height: 14),
Row(
children: [
const Spacer(),
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
final newName = nameCtl.text.trim().isEmpty ? _email : nameCtl.text.trim();
final newEmail = emailCtl.text.trim().isEmpty ? _email : emailCtl.text.trim();
// If user changes email here (edge-case) we will treat newEmail as current account pointer.
final newName = nameCtl.text.trim().isEmpty
? _email
: nameCtl.text.trim();
final newEmail = emailCtl.text.trim().isEmpty
? _email
: emailCtl.text.trim();
await _saveProfile(newName, newEmail, _profileImage);
Navigator.of(context).pop();
},
@@ -274,7 +329,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
const SizedBox(height: 8),
Text(
'Tip: tap the camera icon to pick from gallery (mobile). Or tap the link icon to paste an asset path or URL.',
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
style: theme.textTheme.bodySmall
?.copyWith(color: theme.hintColor),
),
],
),
@@ -286,77 +342,157 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
// ---------- UI helpers ----------
Widget _topIcon(IconData icon, {VoidCallback? onTap}) {
final theme = Theme.of(context);
return InkWell(
onTap: onTap ?? () {},
borderRadius: BorderRadius.circular(12),
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(color: theme.cardColor.withOpacity(0.6), borderRadius: BorderRadius.circular(12)),
child: Icon(icon, color: theme.iconTheme.color),
),
);
// ───────── Avatar builder (reused, with size param) ─────────
Widget _buildProfileAvatar({double size = 96}) {
final path = _profileImage.trim();
if (path.startsWith('http')) {
return ClipOval(
child: Image.network(path,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Icon(Icons.person, size: size / 2, color: Colors.grey)));
}
if (kIsWeb) {
return ClipOval(
child: Image.asset(
path.isNotEmpty ? path : 'assets/images/profile.jpg',
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Icon(Icons.person, size: size / 2, color: Colors.grey)));
}
if (path.isNotEmpty &&
(path.startsWith('/') || path.contains(Platform.pathSeparator))) {
final file = File(path);
if (file.existsSync()) {
return ClipOval(
child: Image.file(file,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Icon(Icons.person, size: size / 2, color: Colors.grey)));
}
}
return ClipOval(
child: Image.asset('assets/images/profile.jpg',
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Icon(Icons.person, size: size / 2, color: Colors.grey)));
}
// ───────── Event list tile (updated styling) ─────────
Widget _eventListTileFromModel(EventModel ev, {bool faded = false}) {
final theme = Theme.of(context);
final title = ev.title ?? ev.name ?? '';
final dateLabel = (ev.startDate != null && ev.endDate != null && ev.startDate == ev.endDate)
final dateLabel =
(ev.startDate != null && ev.endDate != null && ev.startDate == ev.endDate)
? ev.startDate!
: ((ev.startDate != null && ev.endDate != null) ? '${ev.startDate} - ${ev.endDate}' : (ev.startDate ?? ''));
: ((ev.startDate != null && ev.endDate != null)
? '${ev.startDate} - ${ev.endDate}'
: (ev.startDate ?? ''));
final location = ev.place ?? '';
final imageUrl = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) ? ev.thumbImg! : (ev.images.isNotEmpty ? ev.images.first.image : null);
final imageUrl = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty)
? ev.thumbImg!
: (ev.images.isNotEmpty ? ev.images.first.image : null);
final titleStyle = theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600, color: faded ? theme.hintColor : theme.textTheme.bodyLarge?.color);
final subtitleStyle = theme.textTheme.bodySmall?.copyWith(fontSize: 13, color: faded ? theme.hintColor.withOpacity(0.7) : theme.hintColor);
final titleStyle = theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
color: faded ? theme.hintColor : theme.textTheme.bodyLarge?.color);
final subtitleStyle = theme.textTheme.bodySmall?.copyWith(
fontSize: 13,
color: faded
? theme.hintColor.withValues(alpha: 0.7)
: theme.hintColor);
Widget leadingWidget() {
if (imageUrl != null && imageUrl.trim().isNotEmpty) {
if (imageUrl.startsWith('http')) {
return ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.network(imageUrl, width: 56, height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 56, height: 56, color: Theme.of(context).dividerColor, child: Icon(Icons.image, color: Theme.of(context).hintColor))));
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(imageUrl,
width: 60,
height: 60,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 60,
height: 60,
color: theme.dividerColor,
child: Icon(Icons.image, color: theme.hintColor))));
} else if (!kIsWeb) {
final path = imageUrl;
if (path.startsWith('/') || path.contains(Platform.pathSeparator)) {
final file = File(path);
if (file.existsSync()) {
return ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.file(file, width: 56, height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 56, height: 56, color: Theme.of(context).dividerColor, child: Icon(Icons.image, color: Theme.of(context).hintColor))));
} else {
return ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.asset(imageUrl, width: 56, height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 56, height: 56, color: Theme.of(context).dividerColor, child: Icon(Icons.image, color: Theme.of(context).hintColor))));
}
} else {
return ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.asset(imageUrl, width: 56, height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 56, height: 56, color: Theme.of(context).dividerColor, child: Icon(Icons.image, color: Theme.of(context).hintColor))));
}
} else {
return ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.asset(imageUrl, width: 56, height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 56, height: 56, color: Theme.of(context).dividerColor, child: Icon(Icons.image, color: Theme.of(context).hintColor))));
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(file,
width: 60,
height: 60,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 60,
height: 60,
color: theme.dividerColor,
child: Icon(Icons.image, color: theme.hintColor))));
}
}
return Container(width: 56, height: 56, color: Theme.of(context).dividerColor, child: Icon(Icons.image, color: Theme.of(context).hintColor));
}
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(imageUrl,
width: 60,
height: 60,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 60,
height: 60,
color: theme.dividerColor,
child: Icon(Icons.image, color: theme.hintColor))));
}
return Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.image, color: theme.hintColor));
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(14),
boxShadow: [BoxShadow(color: Theme.of(context).shadowColor.withOpacity(0.06), blurRadius: 10, offset: const Offset(0, 6))],
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 15,
offset: const Offset(0, 4))
],
),
child: ListTile(
onTap: () {
if (ev.id != null) {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id)));
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id)));
}
},
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
leading: ClipRRect(borderRadius: BorderRadius.circular(8), child: leadingWidget()),
leading: leadingWidget(),
title: Text(title, style: titleStyle),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 6),
const SizedBox(height: 4),
Text(dateLabel, style: subtitleStyle),
const SizedBox(height: 2),
Text(location, style: subtitleStyle),
@@ -365,118 +501,433 @@ class _ProfileScreenState extends State<ProfileScreen> {
trailing: Container(
width: 40,
height: 40,
decoration: BoxDecoration(color: Theme.of(context).colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(10)),
child: Icon(Icons.qr_code_scanner, color: Theme.of(context).colorScheme.primary),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10)),
child: Icon(Icons.qr_code_scanner, color: theme.colorScheme.primary),
),
),
);
}
Widget _buildProfileAvatar() {
final path = _profileImage.trim();
if (path.startsWith('http')) {
return ClipOval(child: Image.network(path, width: 96, height: 96, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.person, size: 48, color: Colors.grey)));
}
if (kIsWeb) {
return ClipOval(child: Image.asset(path.isNotEmpty ? path : 'assets/images/profile.jpg', width: 96, height: 96, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.person, size: 48, color: Colors.grey)));
}
if (path.isNotEmpty && (path.startsWith('/') || path.contains(Platform.pathSeparator))) {
final file = File(path);
if (file.existsSync()) {
return ClipOval(child: Image.file(file, width: 96, height: 96, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.person, size: 48, color: Colors.grey)));
} else {
return ClipOval(child: Image.asset('assets/images/profile.jpg', width: 96, height: 96, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.person, size: 48, color: Colors.grey)));
}
}
return ClipOval(child: Image.asset('assets/images/profile.jpg', width: 96, height: 96, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.person, size: 48, color: Colors.grey)));
}
// ═══════════════════════════════════════════════
// NEW UI WIDGETS — matching web profile layout
// ═══════════════════════════════════════════════
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final gradient = const LinearGradient(colors: [Color(0xFF0B63D6), Color(0xFF1449B8)], begin: Alignment.topLeft, end: Alignment.bottomRight);
// ───────── Gradient Header ─────────
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Column(
children: [
Container(
Widget _buildGradientHeader(BuildContext context, double height) {
return Container(
width: double.infinity,
decoration: AppDecoration.blueGradient.copyWith(borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24))),
height: height,
decoration: AppDecoration.blueGradient.copyWith(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(40),
bottomRight: Radius.circular(40),
),
),
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
child: Column(
children: [
Row(
children: [
const Expanded(child: SizedBox()),
Text('Profile', style: theme.textTheme.titleMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w600)),
Expanded(
padding: const EdgeInsets.fromLTRB(20, 14, 20, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 40), // balance
const Spacer(),
Text(
'Profile',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white, fontWeight: FontWeight.w600),
),
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),
),
),
],
),
),
),
);
}
// ───────── Cover Banner ─────────
Widget _buildCoverBanner() {
return Stack(
children: [
Container(
height: 160,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: const LinearGradient(
colors: [
Color(0x991E40B1), // rgba(30,64,175,0.6)
Color(0x3394A3B8), // rgba(148,163,184,0.2)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
image: const DecorationImage(
image: AssetImage('assets/images/gradient_dark_blue.png'),
fit: BoxFit.cover,
opacity: 0.5,
),
),
),
// Edit pencil button (top-right)
Positioned(
top: 8,
right: 8,
child: GestureDetector(
onTap: () => _openEditDialog(),
child: Container(margin: const EdgeInsets.only(right: 10), width: 40, height: 40, decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.edit, color: Colors.white)),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
// <-- REPLACED the reminder icon with settings icon and navigation
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)),
child: const Icon(Icons.edit, color: Colors.white, size: 18),
),
],
),
),
],
),
);
}
const SizedBox(height: 18),
// ───────── Avatar overlapping banner + EXP bar ─────────
GestureDetector(
Widget _buildAvatarSection() {
// Use a Stack so the avatar overlaps the bottom of the banner
return Stack(
clipBehavior: Clip.none,
children: [
// Banner
_buildCoverBanner(),
// Avatar positioned at bottom-left, half overlapping
Positioned(
bottom: -36,
left: 16,
child: GestureDetector(
onTap: () => _openEditDialog(),
child: CircleAvatar(radius: 48, backgroundColor: Colors.white, child: _buildProfileAvatar()),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
const SizedBox(height: 12),
Text(_username, style: theme.textTheme.titleLarge?.copyWith(color: Colors.white, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
Container(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(16)), child: Text(_email, style: const TextStyle(color: Colors.white70))),
],
),
child: _buildProfileAvatar(size: 80),
),
),
),
],
);
}
// ───────── EXP Progress Bar ─────────
Widget _buildExpBar() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text(
'exp.',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey.shade500,
),
),
const SizedBox(width: 8),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 24),
child: Container(
height: 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
gradient: _expGradient,
),
),
),
],
),
);
}
// ───────── Stats Row ─────────
Widget _buildStatsRow() {
Widget statColumn(String value, String label) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppConstants.textPrimary,
),
),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w300,
color: AppConstants.textSecondary,
),
),
],
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: IntrinsicHeight(
child: Row(
children: [
statColumn('1.2K', 'Likes'),
VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1),
statColumn('45', 'Posts'),
VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1),
statColumn('3.4K', 'Views'),
],
),
),
);
}
// ───────── Social Icons Row ─────────
Widget _buildSocialIcons() {
final icons = [
Icons.dashboard_outlined,
Icons.camera_alt_outlined,
Icons.alternate_email,
Icons.layers_outlined,
];
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: icons.map((icon) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Icon(icon, size: 24, color: Colors.grey.shade500),
);
}).toList(),
);
}
// ───────── Rainbow Bar ─────────
Widget _buildRainbowBar() {
return Container(
height: 4,
margin: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
gradient: _expGradient,
),
);
}
// ───────── Profile Card (white, overlapping header) ─────────
Widget _buildProfileCard(BuildContext context) {
final theme = Theme.of(context);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(32),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 15,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Removed Upcoming Events section — kept only Past Events below
// Cover Banner + Avatar
_buildAvatarSection(),
Text('Past Events', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
// Spacer for avatar overflow (avatar extends 36px below banner)
const SizedBox(height: 44),
// EXP bar
_buildExpBar(),
const SizedBox(height: 16),
// Name
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_username,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppConstants.textPrimary,
),
),
),
const SizedBox(height: 4),
// Email
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_email,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppConstants.textSecondary,
),
),
),
const SizedBox(height: 24),
// Stats Row
_buildStatsRow(),
const SizedBox(height: 24),
// Social Icons
_buildSocialIcons(),
const SizedBox(height: 20),
// Rainbow bar at bottom
_buildRainbowBar(),
],
),
);
}
// ───────── Event Sections ─────────
Widget _buildEventSections(BuildContext context) {
final theme = Theme.of(context);
Widget sectionHeading(String text) {
return Text(
text,
style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w600, fontSize: 18),
);
}
return Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ongoing Events
if (_ongoingEvents.isNotEmpty) ...[
sectionHeading('Ongoing Events'),
const SizedBox(height: 12),
Column(
children:
_ongoingEvents.map((e) => _eventListTileFromModel(e)).toList()),
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)),
child: Text('No past events',
style: theme.textTheme.bodyMedium
?.copyWith(color: theme.hintColor)),
)
else
Column(children: _pastEvents.map((e) => _eventListTileFromModel(e, faded: true)).toList()),
const SizedBox(height: 28),
],
),
),
),
Column(
children: _pastEvents
.map((e) => _eventListTileFromModel(e, faded: true))
.toList()),
],
),
);
}
// ═══════════════════════════════════════════════
// BUILD
// ═══════════════════════════════════════════════
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
const double headerHeight = 200.0;
const double cardTopOffset = 130.0; // card starts overlapping into header
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: SingleChildScrollView(
child: Column(
children: [
// Header + Profile Card overlap using Stack
Stack(
children: [
_buildGradientHeader(context, headerHeight),
Padding(
padding: EdgeInsets.only(top: cardTopOffset),
child: _buildProfileCard(context),
),
],
),
// Event sections
_buildEventSections(context),
const SizedBox(height: 32),
],
),
),
);
}
}