feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten - Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home - Desktop calendar: 60/40 two-column layout with white background - Desktop profile: full-width banner + 3-column event grids - Desktop learn more: hero image + about/venue columns + gallery strip - Desktop settings/contribute: polished to match design system - Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates - Guest access: requiresAuth false on read endpoints - Location fix: show place names instead of lat/lng coordinates - Version 1.6.1+17 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import 'package:share_plus/share_plus.dart';
|
||||
import '../core/app_decoration.dart';
|
||||
import '../features/gamification/models/gamification_models.dart';
|
||||
import '../features/gamification/providers/gamification_provider.dart';
|
||||
import '../widgets/landscape_section_header.dart';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tier colour map
|
||||
@@ -138,156 +139,184 @@ class _ContributeScreenState extends State<ContributeScreen>
|
||||
static const _desktopTabIcons = [Icons.edit_note, null, null];
|
||||
|
||||
Widget _buildDesktopLayout(BuildContext context, GamificationProvider provider) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: RepaintBoundary(
|
||||
child: Container(
|
||||
decoration: AppDecoration.blueGradient,
|
||||
child: _buildContributeLeftPanel(context, provider),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: RepaintBoundary(
|
||||
child: _buildContributeRightPanel(context, provider),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Landscape left panel: contributor info + vertical nav ───────────────
|
||||
Widget _buildContributeLeftPanel(BuildContext context, GamificationProvider provider) {
|
||||
final profile = provider.profile;
|
||||
final tier = profile?.tier ?? ContributorTier.BRONZE;
|
||||
final lifetimeEp = profile?.lifetimeEp ?? 0;
|
||||
final currentEp = profile?.currentEp ?? 0;
|
||||
final currentRp = profile?.currentRp ?? 0;
|
||||
|
||||
// Calculate next tier threshold
|
||||
const thresholds = [0, 100, 500, 1500, 5000];
|
||||
final tierIdx = tier.index;
|
||||
final nextThresh = tierIdx < 4 ? thresholds[tierIdx + 1] : thresholds[4];
|
||||
final prevThresh = thresholds[tierIdx];
|
||||
final progress = tierIdx >= 4 ? 1.0 : (lifetimeEp - prevThresh) / (nextThresh - prevThresh);
|
||||
final tierColor = _tierColors[tier] ?? Colors.white;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Title
|
||||
const Text('Contributor Dashboard',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: Color(0xFF111827))),
|
||||
const SizedBox(height: 6),
|
||||
const Text('Track your impact, earn rewards, and climb the ranks!',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF6B7280))),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Desktop Tab bar (3 tabs in blue pill) ──
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _primary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Row(
|
||||
children: List.generate(_desktopTabs.length, (i) {
|
||||
final isActive = _activeTab == i;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _activeTab = i),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (i == 0) ...[
|
||||
Icon(Icons.edit_note, size: 18,
|
||||
color: isActive ? _primary : Colors.white70),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Text(
|
||||
_desktopTabs[i],
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isActive ? _primary : Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
// Title
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'Contributor\nDashboard',
|
||||
style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w800, height: 1.2),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'Track your impact & earn rewards',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Contributor Level card ──
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
// Contributor Level badge
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF0F45CF), Color(0xFF3B82F6)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: tierColor.withOpacity(0.5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Contributor Level',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 4),
|
||||
const Text('Start earning rewards by contributing!',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 13)),
|
||||
],
|
||||
Row(children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: tierColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: tierColor.withOpacity(0.6)),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(tierLabel(tier),
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('$lifetimeEp pts',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
if (tierIdx < 4)
|
||||
Text('Next: ${tierLabel(ContributorTier.values[tierIdx + 1])} (${thresholds[tierIdx + 1]} pts)',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
child: Text(tierLabel(tier), style: TextStyle(color: tierColor, fontWeight: FontWeight.w700, fontSize: 12)),
|
||||
),
|
||||
const Spacer(),
|
||||
Text('$lifetimeEp pts', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.clamp(0.0, 1.0),
|
||||
minHeight: 8,
|
||||
backgroundColor: Colors.white.withOpacity(0.2),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
minHeight: 6,
|
||||
backgroundColor: Colors.white24,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(tierColor),
|
||||
),
|
||||
),
|
||||
if (tierIdx < 4) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Next: ${tierLabel(ContributorTier.values[tierIdx + 1])} at ${thresholds[tierIdx + 1]} pts',
|
||||
style: const TextStyle(color: Colors.white54, fontSize: 11),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Desktop tab body ──
|
||||
_buildDesktopTabBody(context, provider),
|
||||
],
|
||||
),
|
||||
// Vertical tab navigation
|
||||
...List.generate(_desktopTabs.length, (i) {
|
||||
final isActive = _activeTab == i;
|
||||
final icons = [Icons.edit_note, Icons.leaderboard_outlined, Icons.emoji_events_outlined];
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _activeTab = i),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.white : Colors.white.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(children: [
|
||||
Icon(icons[i], size: 20, color: isActive ? _primary : Colors.white70),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
_desktopTabs[i],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: isActive ? _primary : Colors.white,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (isActive) Icon(Icons.chevron_right, size: 18, color: _primary),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Landscape right panel: active tab content ────────────────────────────
|
||||
Widget _buildContributeRightPanel(BuildContext context, GamificationProvider provider) {
|
||||
String title;
|
||||
String subtitle;
|
||||
switch (_activeTab) {
|
||||
case 1:
|
||||
title = 'Leaderboard';
|
||||
subtitle = 'Top contributors this month';
|
||||
break;
|
||||
case 2:
|
||||
title = 'Achievements';
|
||||
subtitle = 'Your earned badges';
|
||||
break;
|
||||
default:
|
||||
title = 'Submit Event';
|
||||
subtitle = 'Share events with the community';
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
LandscapeSectionHeader(title: title, subtitle: subtitle),
|
||||
Expanded(
|
||||
child: RepaintBoundary(
|
||||
child: _buildDesktopTabBody(context, provider),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopTabBody(BuildContext context, GamificationProvider provider) {
|
||||
switch (_activeTab) {
|
||||
case 0:
|
||||
|
||||
Reference in New Issue
Block a user