2026-01-31 15:23:18 +05:30
|
|
|
|
// lib/screens/home_screen.dart
|
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
import 'dart:ui';
|
2026-04-19 21:40:17 +05:30
|
|
|
|
import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2026-03-20 22:40:50 +05:30
|
|
|
|
import '../core/auth/auth_guard.dart';
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
import 'package:intl/intl.dart';
|
2026-04-04 16:51:30 +05:30
|
|
|
|
import 'package:table_calendar/table_calendar.dart';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
import '../features/events/models/event_models.dart';
|
|
|
|
|
|
import '../features/events/services/events_service.dart';
|
|
|
|
|
|
import 'calendar_screen.dart';
|
|
|
|
|
|
import 'profile_screen.dart';
|
|
|
|
|
|
import 'contribute_screen.dart';
|
|
|
|
|
|
import 'learn_more_screen.dart';
|
|
|
|
|
|
import 'search_screen.dart';
|
|
|
|
|
|
import '../core/app_decoration.dart';
|
2026-03-21 13:30:05 +05:30
|
|
|
|
import 'package:geocoding/geocoding.dart';
|
2026-03-11 20:13:13 +05:30
|
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
|
|
import '../features/gamification/providers/gamification_provider.dart';
|
feat: Phase 1 critical gaps — gamification API, Razorpay checkout, Google OAuth, notifications
- Fix gamification endpoints to use Node.js server (app.eventifyplus.com)
- Replace 6 mock gamification methods with real API calls (dashboard, leaderboard, shop, redeem, submit)
- Add booking models, service, payment service (Razorpay), checkout provider
- Add 3-step CheckoutScreen with Razorpay native modal integration
- Add Google OAuth login (Flutter + Django backend)
- Add full notifications system (Django model + 3 endpoints + Flutter UI)
- Register CheckoutProvider, NotificationProvider in main.dart MultiProvider
- Wire notification bell in HomeScreen app bar
- Add razorpay_flutter ^1.3.7 and google_sign_in ^6.2.2 packages
2026-04-04 15:46:53 +05:30
|
|
|
|
import '../features/notifications/widgets/notification_bell.dart';
|
|
|
|
|
|
import '../features/notifications/providers/notification_provider.dart';
|
2026-04-04 16:51:30 +05:30
|
|
|
|
import '../widgets/skeleton_loader.dart';
|
2026-04-04 18:45:19 +05:30
|
|
|
|
import '../core/analytics/posthog_service.dart';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
|
|
|
|
const HomeScreen({Key? key}) : super(key: key);
|
|
|
|
|
|
@override
|
|
|
|
|
|
_HomeScreenState createState() => _HomeScreenState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Main screen that hosts 4 tabs in an IndexedStack (Home, Calendar, Contribute, Profile).
|
|
|
|
|
|
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
|
|
|
|
|
|
int _selectedIndex = 0;
|
|
|
|
|
|
String _username = '';
|
|
|
|
|
|
String _location = '';
|
|
|
|
|
|
String _pincode = 'all';
|
2026-04-04 18:43:02 +05:30
|
|
|
|
double? _userLat;
|
|
|
|
|
|
double? _userLng;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
final EventsService _eventsService = EventsService();
|
|
|
|
|
|
|
|
|
|
|
|
// backend-driven
|
2026-03-30 10:05:23 +05:30
|
|
|
|
List<EventModel> _allEvents = []; // master copy, never filtered
|
2026-01-31 15:23:18 +05:30
|
|
|
|
List<EventModel> _events = [];
|
2026-04-08 21:12:49 +05:30
|
|
|
|
List<EventModel> _featuredEvents = [];
|
|
|
|
|
|
List<EventModel> _topEventsList = [];
|
2026-01-31 15:23:18 +05:30
|
|
|
|
List<EventTypeModel> _types = [];
|
|
|
|
|
|
int _selectedTypeId = -1; // -1 == All
|
2026-03-30 20:32:54 +05:30
|
|
|
|
bool _categoriesExpanded = false;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
bool _loading = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Hero carousel
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
final PageController _heroPageController = PageController(viewportFraction: 0.9);
|
2026-03-18 15:39:42 +05:30
|
|
|
|
late final ValueNotifier<int> _heroPageNotifier;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Timer? _autoScrollTimer;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
2026-03-18 15:39:42 +05:30
|
|
|
|
_heroPageNotifier = ValueNotifier(0);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
_loadUserDataAndEvents();
|
2026-04-08 21:12:49 +05:30
|
|
|
|
_loadCuratedEvents();
|
2026-01-31 15:23:18 +05:30
|
|
|
|
_startAutoScroll();
|
2026-04-04 18:45:19 +05:30
|
|
|
|
PostHogService.instance.screen('Home');
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_autoScrollTimer?.cancel();
|
|
|
|
|
|
_heroPageController.dispose();
|
2026-03-18 15:39:42 +05:30
|
|
|
|
_heroPageNotifier.dispose();
|
2026-01-31 15:23:18 +05:30
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
void _startAutoScroll({Duration delay = const Duration(seconds: 5)}) {
|
2026-01-31 15:23:18 +05:30
|
|
|
|
_autoScrollTimer?.cancel();
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
_autoScrollTimer = Timer.periodic(delay, (timer) {
|
2026-01-31 15:23:18 +05:30
|
|
|
|
if (_heroEvents.isEmpty) return;
|
2026-03-18 15:39:42 +05:30
|
|
|
|
final nextPage = (_heroPageNotifier.value + 1) % _heroEvents.length;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
if (_heroPageController.hasClients) {
|
|
|
|
|
|
_heroPageController.animateToPage(
|
|
|
|
|
|
nextPage,
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
duration: const Duration(milliseconds: 400),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
curve: Curves.easeInOut,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _loadUserDataAndEvents() async {
|
|
|
|
|
|
setState(() => _loading = true);
|
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
|
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? '';
|
2026-04-07 20:49:40 +05:30
|
|
|
|
final storedLocation = prefs.getString('location') ?? 'Thrissur';
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
// Fix legacy lat,lng strings saved before the reverse-geocoding fix
|
2026-03-21 13:30:05 +05:30
|
|
|
|
final coordMatch = RegExp(r'^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$').firstMatch(storedLocation);
|
|
|
|
|
|
if (coordMatch != null) {
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
_location = 'Current Location';
|
2026-03-21 13:30:05 +05:30
|
|
|
|
// Reverse geocode in background to get actual place name
|
|
|
|
|
|
_reverseGeocodeAndSave(
|
|
|
|
|
|
double.parse(coordMatch.group(1)!),
|
|
|
|
|
|
double.parse(coordMatch.group(2)!),
|
|
|
|
|
|
prefs,
|
|
|
|
|
|
);
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
} else {
|
|
|
|
|
|
_location = storedLocation;
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
_pincode = prefs.getString('pincode') ?? 'all';
|
2026-04-04 18:43:02 +05:30
|
|
|
|
_userLat = prefs.getDouble('user_lat');
|
|
|
|
|
|
_userLng = prefs.getDouble('user_lng');
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-08 21:12:49 +05:30
|
|
|
|
// Fetch types and location-based events in parallel.
|
2026-04-04 18:43:02 +05:30
|
|
|
|
// Prefer haversine (lat/lng) when GPS coords are available; fall back to pincode.
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
final results = await Future.wait([
|
|
|
|
|
|
_events_service_getEventTypesSafe(),
|
2026-04-04 18:43:02 +05:30
|
|
|
|
(_userLat != null && _userLng != null)
|
|
|
|
|
|
? _events_service_getEventsByLocationSafe(_userLat!, _userLng!)
|
|
|
|
|
|
: _events_service_getEventsSafe(_pincode),
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
]);
|
|
|
|
|
|
final types = results[0] as List<EventTypeModel>;
|
|
|
|
|
|
final events = results[1] as List<EventModel>;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
if (mounted) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_types = types;
|
2026-03-30 10:05:23 +05:30
|
|
|
|
_allEvents = events;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
_events = events;
|
|
|
|
|
|
_selectedTypeId = -1;
|
2026-03-30 10:05:23 +05:30
|
|
|
|
_cachedFilteredEvents = null;
|
|
|
|
|
|
_cachedEventDates = null;
|
|
|
|
|
|
_loading = false;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-04-19 21:40:17 +05:30
|
|
|
|
if (kDebugMode) debugPrint('HomeScreen init unexpected error: $e');
|
|
|
|
|
|
if (mounted) setState(() => _loading = false);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
feat: Phase 1 critical gaps — gamification API, Razorpay checkout, Google OAuth, notifications
- Fix gamification endpoints to use Node.js server (app.eventifyplus.com)
- Replace 6 mock gamification methods with real API calls (dashboard, leaderboard, shop, redeem, submit)
- Add booking models, service, payment service (Razorpay), checkout provider
- Add 3-step CheckoutScreen with Razorpay native modal integration
- Add Google OAuth login (Flutter + Django backend)
- Add full notifications system (Django model + 3 endpoints + Flutter UI)
- Register CheckoutProvider, NotificationProvider in main.dart MultiProvider
- Wire notification bell in HomeScreen app bar
- Add razorpay_flutter ^1.3.7 and google_sign_in ^6.2.2 packages
2026-04-04 15:46:53 +05:30
|
|
|
|
|
|
|
|
|
|
// Refresh notification badge count (fire-and-forget)
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
context.read<NotificationProvider>().refreshUnreadCount();
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 13:30:05 +05:30
|
|
|
|
Future<void> _reverseGeocodeAndSave(double lat, double lng, SharedPreferences prefs) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final placemarks = await placemarkFromCoordinates(lat, lng);
|
|
|
|
|
|
if (placemarks.isNotEmpty) {
|
|
|
|
|
|
final p = placemarks.first;
|
|
|
|
|
|
final parts = <String>[];
|
|
|
|
|
|
if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!);
|
|
|
|
|
|
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
|
|
|
|
|
|
if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!);
|
|
|
|
|
|
final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location';
|
|
|
|
|
|
await prefs.setString('location', label);
|
|
|
|
|
|
if (mounted) setState(() => _location = label);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
await prefs.setString('location', 'Current Location');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Future<List<EventTypeModel>> _events_service_getEventTypesSafe() async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await _eventsService.getEventTypes();
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return <EventTypeModel>[];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<List<EventModel>> _events_service_getEventsSafe(String pincode) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await _eventsService.getEventsByPincode(pincode);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return <EventModel>[];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 18:43:02 +05:30
|
|
|
|
Future<List<EventModel>> _events_service_getEventsByLocationSafe(double lat, double lng) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await _eventsService.getEventsByLocation(lat, lng);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// Fallback to all events if location-based fetch fails
|
|
|
|
|
|
return _events_service_getEventsSafe('all');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 21:12:49 +05:30
|
|
|
|
/// Loads featured carousel + top events once globally — no pincode, never re-fetched on location change.
|
|
|
|
|
|
Future<void> _loadCuratedEvents() async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final results = await Future.wait([
|
|
|
|
|
|
_eventsService.getFeaturedEvents(),
|
|
|
|
|
|
_eventsService.getTopEvents(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_featuredEvents = results[0] as List<EventModel>;
|
|
|
|
|
|
_topEventsList = results[1] as List<EventModel>;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// Non-critical — fallback getters handle empty lists gracefully
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Future<void> _refresh() async {
|
|
|
|
|
|
await _loadUserDataAndEvents();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _bookEventAtIndex(int index) {
|
|
|
|
|
|
if (index >= 0 && index < _events.length) {
|
|
|
|
|
|
setState(() => _events.removeAt(index));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _categoryChip({
|
|
|
|
|
|
required String label,
|
|
|
|
|
|
required bool selected,
|
|
|
|
|
|
required VoidCallback onTap,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
String? imageUrl,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
IconData? icon,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
2026-03-11 20:13:13 +05:30
|
|
|
|
return GestureDetector(
|
2026-01-31 15:23:18 +05:30
|
|
|
|
onTap: onTap,
|
|
|
|
|
|
child: Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
width: 110,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
decoration: BoxDecoration(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
color: selected ? theme.colorScheme.primary : Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
2026-03-30 22:01:46 +05:30
|
|
|
|
border: Border.all(
|
|
|
|
|
|
color: selected ? theme.colorScheme.primary : const Color(0xFFE5E7EB),
|
|
|
|
|
|
width: 1.5,
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: selected
|
2026-03-30 22:01:46 +05:30
|
|
|
|
? theme.colorScheme.primary.withValues(alpha: 0.35)
|
|
|
|
|
|
: Colors.black.withValues(alpha: 0.08),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
blurRadius: selected ? 12 : 8,
|
|
|
|
|
|
offset: const Offset(0, 4),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
children: [
|
2026-03-11 20:13:13 +05:30
|
|
|
|
// Image / Icon area
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: 56,
|
|
|
|
|
|
width: 56,
|
|
|
|
|
|
child: imageUrl != null && imageUrl.isNotEmpty
|
|
|
|
|
|
? ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
child: CachedNetworkImage(
|
|
|
|
|
|
imageUrl: imageUrl,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 112,
|
|
|
|
|
|
memCacheHeight: 112,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
fit: BoxFit.contain,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
placeholder: (_, __) => Icon(
|
|
|
|
|
|
icon ?? Icons.category,
|
|
|
|
|
|
size: 36,
|
|
|
|
|
|
color: selected ? Colors.white.withOpacity(0.5) : theme.colorScheme.primary.withOpacity(0.3),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Icon(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
icon ?? Icons.category,
|
|
|
|
|
|
size: 36,
|
|
|
|
|
|
color: selected ? Colors.white : theme.colorScheme.primary,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Icon(
|
|
|
|
|
|
icon ?? Icons.category,
|
|
|
|
|
|
size: 36,
|
|
|
|
|
|
color: selected ? Colors.white : theme.colorScheme.primary,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
// Label
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
label,
|
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: selected
|
|
|
|
|
|
? Colors.white
|
|
|
|
|
|
: theme.textTheme.bodyLarge?.color ?? Colors.black87,
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _openLocationSearch() async {
|
|
|
|
|
|
final selected = await Navigator.of(context).push(PageRouteBuilder(
|
|
|
|
|
|
opaque: false,
|
|
|
|
|
|
pageBuilder: (context, animation, secondaryAnimation) => const SearchScreen(),
|
|
|
|
|
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
|
|
|
|
return FadeTransition(opacity: animation, child: child);
|
|
|
|
|
|
},
|
|
|
|
|
|
transitionDuration: const Duration(milliseconds: 220),
|
|
|
|
|
|
));
|
|
|
|
|
|
|
2026-04-04 18:43:02 +05:30
|
|
|
|
if (selected != null && selected is Map) {
|
|
|
|
|
|
final label = (selected['label'] as String?) ?? 'Current Location';
|
|
|
|
|
|
final pincode = (selected['pincode'] as String?) ?? 'all';
|
|
|
|
|
|
final lat = selected['lat'] as double?;
|
|
|
|
|
|
final lng = selected['lng'] as double?;
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
2026-04-04 18:43:02 +05:30
|
|
|
|
await prefs.setString('location', label);
|
|
|
|
|
|
await prefs.setString('pincode', pincode);
|
|
|
|
|
|
if (lat != null && lng != null) {
|
|
|
|
|
|
await prefs.setDouble('user_lat', lat);
|
|
|
|
|
|
await prefs.setDouble('user_lng', lng);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await prefs.remove('user_lat');
|
|
|
|
|
|
await prefs.remove('user_lng');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_location = label;
|
|
|
|
|
|
_pincode = pincode;
|
|
|
|
|
|
_userLat = lat;
|
|
|
|
|
|
_userLng = lng;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
await _refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _openEventSearch() {
|
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
isScrollControlled: true,
|
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
|
builder: (ctx) {
|
|
|
|
|
|
return DraggableScrollableSheet(
|
|
|
|
|
|
expand: false,
|
|
|
|
|
|
initialChildSize: 0.6,
|
|
|
|
|
|
minChildSize: 0.3,
|
|
|
|
|
|
maxChildSize: 0.95,
|
|
|
|
|
|
builder: (context, scrollController) {
|
|
|
|
|
|
String query = '';
|
|
|
|
|
|
List<EventModel> results = List.from(_events);
|
2026-04-04 17:33:56 +05:30
|
|
|
|
bool searching = false;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return StatefulBuilder(builder: (context, setModalState) {
|
2026-04-04 17:33:56 +05:30
|
|
|
|
// Instant client-side filter while typing
|
2026-01-31 15:23:18 +05:30
|
|
|
|
void _onQueryChanged(String v) {
|
|
|
|
|
|
query = v.trim().toLowerCase();
|
|
|
|
|
|
final r = _events.where((e) {
|
|
|
|
|
|
final title = (e.title ?? e.name ?? '').toLowerCase();
|
|
|
|
|
|
return title.contains(query);
|
|
|
|
|
|
}).toList();
|
|
|
|
|
|
setModalState(() {
|
|
|
|
|
|
results = r;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 17:33:56 +05:30
|
|
|
|
// Server-side search on submit (keyboard action / enter)
|
|
|
|
|
|
Future<void> _onSubmitted(String v) async {
|
|
|
|
|
|
final q = v.trim();
|
|
|
|
|
|
if (q.isEmpty) return;
|
|
|
|
|
|
setModalState(() => searching = true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
final serverResults = await _eventsService.getEventsByPincode(_pincode, q: q);
|
|
|
|
|
|
setModalState(() {
|
|
|
|
|
|
results = serverResults;
|
|
|
|
|
|
searching = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
setModalState(() => searching = false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: SafeArea(
|
|
|
|
|
|
top: false,
|
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
|
controller: scrollController,
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Center(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 48,
|
|
|
|
|
|
height: 6,
|
|
|
|
|
|
decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(6)),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
|
|
|
|
decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: theme.dividerColor)),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(Icons.search, color: theme.hintColor),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: TextField(
|
|
|
|
|
|
style: theme.textTheme.bodyLarge,
|
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
|
hintText: 'Search events by name',
|
|
|
|
|
|
hintStyle: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
|
|
|
|
|
|
border: InputBorder.none,
|
|
|
|
|
|
),
|
|
|
|
|
|
autofocus: true,
|
|
|
|
|
|
onChanged: _onQueryChanged,
|
|
|
|
|
|
textInputAction: TextInputAction.search,
|
2026-04-04 17:33:56 +05:30
|
|
|
|
onSubmitted: _onSubmitted,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
IconButton(
|
|
|
|
|
|
icon: Icon(Icons.close, color: theme.iconTheme.color),
|
|
|
|
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 12),
|
2026-04-04 17:33:56 +05:30
|
|
|
|
if (_loading || searching)
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Center(child: CircularProgressIndicator(color: theme.colorScheme.primary))
|
|
|
|
|
|
else if (results.isEmpty)
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
|
|
|
|
|
child: Center(child: Text(query.isEmpty ? 'Type to search events' : 'No events found', style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor))),
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
2026-03-18 15:39:42 +05:30
|
|
|
|
ConstrainedBox(
|
|
|
|
|
|
constraints: const BoxConstraints(maxHeight: 400),
|
|
|
|
|
|
child: ListView.separated(
|
|
|
|
|
|
shrinkWrap: false,
|
|
|
|
|
|
physics: const ClampingScrollPhysics(),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
itemBuilder: (ctx, idx) {
|
|
|
|
|
|
final ev = results[idx];
|
|
|
|
|
|
final img = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) ? ev.thumbImg! : (ev.images.isNotEmpty ? ev.images.first.image : null);
|
|
|
|
|
|
final title = ev.title ?? ev.name ?? '';
|
|
|
|
|
|
final subtitle = ev.startDate ?? '';
|
|
|
|
|
|
return ListTile(
|
|
|
|
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
|
|
|
|
|
|
leading: img != null && img.isNotEmpty
|
2026-03-18 15:39:42 +05:30
|
|
|
|
? ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
|
child: CachedNetworkImage(
|
|
|
|
|
|
imageUrl: img,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 112,
|
|
|
|
|
|
memCacheHeight: 112,
|
2026-03-18 15:39:42 +05:30
|
|
|
|
width: 56,
|
|
|
|
|
|
height: 56,
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
placeholder: (_, __) => Container(width: 56, height: 56, color: theme.dividerColor),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(width: 56, height: 56, color: theme.dividerColor, child: Icon(Icons.event, color: theme.hintColor)),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
2026-01-31 15:23:18 +05:30
|
|
|
|
: Container(width: 56, height: 56, decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(8)), child: Icon(Icons.event, color: theme.hintColor)),
|
|
|
|
|
|
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge),
|
|
|
|
|
|
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)),
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
|
if (ev.id != null) {
|
2026-04-07 20:49:40 +05:30
|
|
|
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev, heroTag: 'event-hero-${ev.id}-search')));
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
separatorBuilder: (_, __) => Divider(color: theme.dividerColor),
|
|
|
|
|
|
itemCount: results.length,
|
2026-03-18 15:39:42 +05:30
|
|
|
|
)), // ConstrainedBox
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
body: Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// IndexedStack keeps each tab alive and preserves state.
|
2026-03-18 15:39:42 +05:30
|
|
|
|
// RepaintBoundary isolates each tab so inactive tabs don't trigger repaints.
|
2026-01-31 15:23:18 +05:30
|
|
|
|
IndexedStack(
|
|
|
|
|
|
index: _selectedIndex,
|
|
|
|
|
|
children: [
|
2026-03-18 15:39:42 +05:30
|
|
|
|
RepaintBoundary(child: _buildHomeContent()), // index 0
|
|
|
|
|
|
const RepaintBoundary(child: CalendarScreen()), // index 1
|
feat: Phase 1 critical gaps — gamification API, Razorpay checkout, Google OAuth, notifications
- Fix gamification endpoints to use Node.js server (app.eventifyplus.com)
- Replace 6 mock gamification methods with real API calls (dashboard, leaderboard, shop, redeem, submit)
- Add booking models, service, payment service (Razorpay), checkout provider
- Add 3-step CheckoutScreen with Razorpay native modal integration
- Add Google OAuth login (Flutter + Django backend)
- Add full notifications system (Django model + 3 endpoints + Flutter UI)
- Register CheckoutProvider, NotificationProvider in main.dart MultiProvider
- Wire notification bell in HomeScreen app bar
- Add razorpay_flutter ^1.3.7 and google_sign_in ^6.2.2 packages
2026-04-04 15:46:53 +05:30
|
|
|
|
const RepaintBoundary(child: ContributeScreen()), // index 2 (full page, scrollable)
|
2026-03-18 15:39:42 +05:30
|
|
|
|
const RepaintBoundary(child: ProfileScreen()), // index 3
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Floating bottom navigation (always visible)
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
// bottom offset accounts for home indicator on iPhone/Android gesture bar
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 16,
|
|
|
|
|
|
right: 16,
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
child: _buildFloatingBottomNav(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildFloatingBottomNav() {
|
2026-03-14 08:55:21 +05:30
|
|
|
|
const activeColor = Color(0xFF2563EB);
|
|
|
|
|
|
const inactiveColor = Color(0xFF9CA3AF);
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return Container(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
decoration: BoxDecoration(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 16, offset: const Offset(0, -2)),
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
|
|
|
|
children: [
|
2026-03-14 08:55:21 +05:30
|
|
|
|
_bottomNavItem(0, Icons.home_outlined, Icons.home, 'Home'),
|
|
|
|
|
|
_bottomNavItem(1, Icons.calendar_today_outlined, Icons.calendar_today, 'Calendar'),
|
|
|
|
|
|
_bottomNavItem(2, Icons.front_hand_outlined, Icons.front_hand, 'Contribute'),
|
|
|
|
|
|
_bottomNavItem(3, Icons.person_outline, Icons.person, 'Profile'),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
Widget _bottomNavItem(int index, IconData outlinedIcon, IconData filledIcon, String label) {
|
|
|
|
|
|
const activeColor = Color(0xFF2563EB);
|
|
|
|
|
|
const inactiveColor = Color(0xFF9CA3AF);
|
|
|
|
|
|
final active = _selectedIndex == index;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return GestureDetector(
|
2026-03-20 22:40:50 +05:30
|
|
|
|
onTap: () {
|
|
|
|
|
|
if (index == 2 && !AuthGuard.requireLogin(context, reason: 'Sign in to contribute events and earn rewards.')) return;
|
|
|
|
|
|
if (index == 3 && !AuthGuard.requireLogin(context, reason: 'Sign in to view your profile.')) return;
|
|
|
|
|
|
setState(() => _selectedIndex = index);
|
|
|
|
|
|
},
|
2026-03-14 08:55:21 +05:30
|
|
|
|
behavior: HitTestBehavior.opaque,
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(
|
|
|
|
|
|
active ? filledIcon : outlinedIcon,
|
|
|
|
|
|
color: active ? activeColor : inactiveColor,
|
|
|
|
|
|
size: 24,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
label,
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: active ? activeColor : inactiveColor,
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
2026-04-08 21:12:49 +05:30
|
|
|
|
// Featured events for the hero carousel — from dedicated endpoint, fallback to first 6
|
|
|
|
|
|
List<EventModel> get _heroEvents =>
|
|
|
|
|
|
_featuredEvents.isNotEmpty ? _featuredEvents : _allEvents.take(6).toList();
|
|
|
|
|
|
|
|
|
|
|
|
// Top events respecting the active date filter — from dedicated endpoint, fallback to date-filtered all
|
|
|
|
|
|
List<EventModel> get _topEventsFiltered {
|
|
|
|
|
|
if (_topEventsList.isEmpty) return _allFilteredByDate;
|
|
|
|
|
|
if (_selectedDateFilter.isEmpty) return _topEventsList;
|
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
|
|
|
|
DateTime filterStart;
|
|
|
|
|
|
DateTime filterEnd;
|
|
|
|
|
|
switch (_selectedDateFilter) {
|
|
|
|
|
|
case 'Today':
|
|
|
|
|
|
filterStart = today;
|
|
|
|
|
|
filterEnd = today;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Tomorrow':
|
|
|
|
|
|
filterStart = today.add(const Duration(days: 1));
|
|
|
|
|
|
filterEnd = filterStart;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'This week':
|
|
|
|
|
|
filterStart = today;
|
|
|
|
|
|
filterEnd = today.add(Duration(days: 7 - today.weekday));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Date':
|
|
|
|
|
|
if (_selectedCustomDate == null) return _topEventsList;
|
|
|
|
|
|
filterStart = DateTime(_selectedCustomDate!.year, _selectedCustomDate!.month, _selectedCustomDate!.day);
|
|
|
|
|
|
filterEnd = filterStart;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return _topEventsList;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _topEventsList.where((e) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final s = DateTime.parse(e.startDate);
|
|
|
|
|
|
final eEnd = DateTime.parse(e.endDate);
|
|
|
|
|
|
final eStart = DateTime(s.year, s.month, s.day);
|
|
|
|
|
|
final eEndDay = DateTime(eEnd.year, eEnd.month, eEnd.day);
|
|
|
|
|
|
return !eEndDay.isBefore(filterStart) && !eStart.isAfter(filterEnd);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}).toList();
|
|
|
|
|
|
}
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
|
|
|
|
|
|
String _formatDate(String dateStr) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final dt = DateTime.parse(dateStr);
|
|
|
|
|
|
return DateFormat('d MMM yyyy').format(dt);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return dateStr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String _getEventTypeName(EventModel event) {
|
|
|
|
|
|
if (event.eventTypeId != null && event.eventTypeId! > 0) {
|
|
|
|
|
|
final match = _types.where((t) => t.id == event.eventTypeId);
|
|
|
|
|
|
if (match.isNotEmpty && match.first.name.isNotEmpty) {
|
|
|
|
|
|
return match.first.name.toUpperCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'EVENT';
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
// Date filter state
|
|
|
|
|
|
String _selectedDateFilter = '';
|
|
|
|
|
|
DateTime? _selectedCustomDate;
|
|
|
|
|
|
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
// Cached filtered events to avoid repeated DateTime.parse() calls
|
|
|
|
|
|
List<EventModel>? _cachedFilteredEvents;
|
|
|
|
|
|
String _cachedFilterKey = '';
|
|
|
|
|
|
|
2026-03-30 10:05:23 +05:30
|
|
|
|
// Cached event dates for calendar dots
|
|
|
|
|
|
Set<DateTime>? _cachedEventDates;
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns all events filtered by date only (ignores category selection).
|
|
|
|
|
|
/// Used by Top Events and category sections so they always show all types.
|
|
|
|
|
|
List<EventModel> get _allFilteredByDate {
|
|
|
|
|
|
if (_selectedDateFilter.isEmpty) return _allEvents;
|
|
|
|
|
|
// Reuse the same date-filter logic as _filteredEvents but on _allEvents
|
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
|
|
|
|
DateTime filterStart;
|
|
|
|
|
|
DateTime filterEnd;
|
|
|
|
|
|
switch (_selectedDateFilter) {
|
|
|
|
|
|
case 'Today':
|
|
|
|
|
|
filterStart = today;
|
|
|
|
|
|
filterEnd = today;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Tomorrow':
|
|
|
|
|
|
filterStart = today.add(const Duration(days: 1));
|
|
|
|
|
|
filterEnd = filterStart;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'This week':
|
|
|
|
|
|
filterStart = today;
|
|
|
|
|
|
filterEnd = today.add(Duration(days: 7 - today.weekday));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Date':
|
|
|
|
|
|
if (_selectedCustomDate == null) return _allEvents;
|
|
|
|
|
|
filterStart = DateTime(_selectedCustomDate!.year, _selectedCustomDate!.month, _selectedCustomDate!.day);
|
|
|
|
|
|
filterEnd = filterStart;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return _allEvents;
|
|
|
|
|
|
}
|
|
|
|
|
|
return _allEvents.where((e) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final s = DateTime.parse(e.startDate);
|
|
|
|
|
|
final eEnd = DateTime.parse(e.endDate);
|
|
|
|
|
|
final eStart = DateTime(s.year, s.month, s.day);
|
|
|
|
|
|
final eEndDay = DateTime(eEnd.year, eEnd.month, eEnd.day);
|
|
|
|
|
|
return !eEndDay.isBefore(filterStart) && !eStart.isAfter(filterEnd);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}).toList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
/// Returns the subset of [_events] that match the active date-filter chip.
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
/// Uses caching to avoid re-parsing dates on every access.
|
2026-03-14 08:55:21 +05:30
|
|
|
|
List<EventModel> get _filteredEvents {
|
|
|
|
|
|
if (_selectedDateFilter.isEmpty) return _events;
|
|
|
|
|
|
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
// Build a cache key from filter state
|
|
|
|
|
|
final cacheKey = '$_selectedDateFilter|$_selectedCustomDate|${_events.length}';
|
|
|
|
|
|
if (_cachedFilteredEvents != null && _cachedFilterKey == cacheKey) {
|
|
|
|
|
|
return _cachedFilteredEvents!;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
|
|
|
|
|
|
|
|
|
|
DateTime filterStart;
|
|
|
|
|
|
DateTime filterEnd;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
switch (_selectedDateFilter) {
|
|
|
|
|
|
case 'Today':
|
|
|
|
|
|
filterStart = today;
|
|
|
|
|
|
filterEnd = today;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Tomorrow':
|
|
|
|
|
|
final tomorrow = today.add(const Duration(days: 1));
|
|
|
|
|
|
filterStart = tomorrow;
|
|
|
|
|
|
filterEnd = tomorrow;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'This week':
|
|
|
|
|
|
// Monday–Sunday of the current week
|
|
|
|
|
|
final weekday = today.weekday; // 1=Mon
|
|
|
|
|
|
filterStart = today.subtract(Duration(days: weekday - 1));
|
|
|
|
|
|
filterEnd = filterStart.add(const Duration(days: 6));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Date':
|
|
|
|
|
|
if (_selectedCustomDate == null) return _events;
|
|
|
|
|
|
filterStart = _selectedCustomDate!;
|
|
|
|
|
|
filterEnd = _selectedCustomDate!;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return _events;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
_cachedFilteredEvents = _events.where((e) {
|
2026-03-14 08:55:21 +05:30
|
|
|
|
try {
|
|
|
|
|
|
final eStart = DateTime.parse(e.startDate);
|
|
|
|
|
|
final eEnd = DateTime.parse(e.endDate);
|
|
|
|
|
|
// Event overlaps with filter range
|
|
|
|
|
|
return !eEnd.isBefore(filterStart) && !eStart.isAfter(filterEnd);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return false;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
2026-03-14 08:55:21 +05:30
|
|
|
|
}).toList();
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
_cachedFilterKey = cacheKey;
|
|
|
|
|
|
return _cachedFilteredEvents!;
|
2026-03-14 08:55:21 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _onDateChipTap(String label) async {
|
|
|
|
|
|
if (label == 'Date') {
|
|
|
|
|
|
// Open custom calendar dialog
|
2026-04-04 16:51:30 +05:30
|
|
|
|
final picked = await _showCalendarBottomSheet();
|
2026-03-14 08:55:21 +05:30
|
|
|
|
if (picked != null) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_selectedCustomDate = picked;
|
|
|
|
|
|
_selectedDateFilter = 'Date';
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
_cachedFilteredEvents = null; // invalidate cache
|
2026-03-14 08:55:21 +05:30
|
|
|
|
});
|
2026-03-14 20:15:55 +05:30
|
|
|
|
_showFilteredEventsSheet(
|
|
|
|
|
|
'${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}',
|
|
|
|
|
|
);
|
2026-03-14 08:55:21 +05:30
|
|
|
|
} else if (_selectedDateFilter == 'Date') {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_selectedDateFilter = '';
|
|
|
|
|
|
_selectedCustomDate = null;
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
_cachedFilteredEvents = null;
|
2026-03-14 08:55:21 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setState(() {
|
2026-03-14 20:15:55 +05:30
|
|
|
|
_selectedDateFilter = label;
|
|
|
|
|
|
_selectedCustomDate = null;
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
_cachedFilteredEvents = null; // invalidate cache
|
2026-03-14 20:15:55 +05:30
|
|
|
|
});
|
|
|
|
|
|
_showFilteredEventsSheet(label);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String _monthName(int m) {
|
|
|
|
|
|
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
|
|
|
|
return months[m - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Shows a bottom sheet with events matching the current filter chip.
|
|
|
|
|
|
void _showFilteredEventsSheet(String title) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
final filtered = _filteredEvents;
|
|
|
|
|
|
final count = filtered.length;
|
|
|
|
|
|
|
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
|
context: context,
|
|
|
|
|
|
isScrollControlled: true,
|
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
|
barrierColor: Colors.black.withValues(alpha: 0.5),
|
|
|
|
|
|
builder: (ctx) {
|
|
|
|
|
|
return DraggableScrollableSheet(
|
|
|
|
|
|
expand: false,
|
|
|
|
|
|
initialChildSize: 0.55,
|
|
|
|
|
|
minChildSize: 0.3,
|
|
|
|
|
|
maxChildSize: 0.85,
|
|
|
|
|
|
builder: (context, scrollController) {
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
color: Color(0xFFEAEFFE), // lavender sheet bg matching web
|
|
|
|
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Drag handle
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 5,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.grey.shade400,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(3),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Header row: title + close button
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 4, 12, 12),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
'$title ($count)',
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: Color(0xFF1A1A1A),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_selectedDateFilter = '';
|
|
|
|
|
|
_selectedCustomDate = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 32,
|
|
|
|
|
|
height: 32,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.grey.shade300,
|
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Icon(Icons.close, size: 18, color: Color(0xFF1A1A1A)),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Events list
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: filtered.isEmpty
|
|
|
|
|
|
? Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(24),
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Text(
|
|
|
|
|
|
'\u{1F3D7}\u{FE0F} No events scheduled for this period',
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
|
color: Color(0xFF6B7280),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: ListView.builder(
|
|
|
|
|
|
controller: scrollController,
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
|
|
|
|
|
itemCount: filtered.length,
|
|
|
|
|
|
itemBuilder: (ctx, idx) {
|
|
|
|
|
|
final ev = filtered[idx];
|
|
|
|
|
|
return _buildSheetEventCard(ev, theme);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
).whenComplete(() {
|
|
|
|
|
|
// Clear filter when sheet is dismissed
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_selectedDateFilter = '';
|
2026-03-14 08:55:21 +05:30
|
|
|
|
_selectedCustomDate = null;
|
|
|
|
|
|
});
|
2026-03-14 20:15:55 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Builds an event card for the filter bottom sheet, matching web design.
|
|
|
|
|
|
Widget _buildSheetEventCard(EventModel ev, ThemeData theme) {
|
|
|
|
|
|
final title = ev.title ?? ev.name ?? '';
|
|
|
|
|
|
final dateLabel = ev.startDate ?? '';
|
|
|
|
|
|
final location = ev.place ?? 'Location';
|
|
|
|
|
|
final imageUrl = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty)
|
|
|
|
|
|
? ev.thumbImg!
|
|
|
|
|
|
: (ev.images.isNotEmpty ? ev.images.first.image : null);
|
|
|
|
|
|
|
|
|
|
|
|
Widget imageWidget;
|
|
|
|
|
|
if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('http')) {
|
|
|
|
|
|
imageWidget = ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
child: CachedNetworkImage(
|
|
|
|
|
|
imageUrl: imageUrl,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 160,
|
|
|
|
|
|
memCacheHeight: 160,
|
2026-03-14 20:15:55 +05:30
|
|
|
|
width: 80,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
fit: BoxFit.cover,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
width: 80, height: 80,
|
|
|
|
|
|
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
|
|
|
|
|
|
child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
2026-03-14 20:15:55 +05:30
|
|
|
|
width: 80, height: 80,
|
|
|
|
|
|
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
|
|
|
|
|
|
child: Icon(Icons.image, color: Colors.grey.shade400),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
imageWidget = Container(
|
|
|
|
|
|
width: 80, height: 80,
|
|
|
|
|
|
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
|
|
|
|
|
|
child: Icon(Icons.image, color: Colors.grey.shade400),
|
|
|
|
|
|
);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
2026-03-14 20:15:55 +05:30
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
|
if (ev.id != null) {
|
2026-04-07 20:49:40 +05:30
|
|
|
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev, heroTag: 'event-hero-${ev.id}-sheet')));
|
2026-03-14 20:15:55 +05:30
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: Colors.black.withValues(alpha: 0.04),
|
|
|
|
|
|
blurRadius: 8,
|
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
imageWidget,
|
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
title,
|
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: Color(0xFF1A1A1A),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(Icons.calendar_today, size: 13, color: Colors.grey.shade500),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
dateLabel,
|
|
|
|
|
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(Icons.location_on_outlined, size: 13, color: Colors.grey.shade500),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
location,
|
|
|
|
|
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade500),
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'Free',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-03-14 08:55:21 +05:30
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
/// Collect all event dates (start + end range) to show dots on the calendar.
|
2026-03-30 10:05:23 +05:30
|
|
|
|
/// Cached to avoid re-parsing on every calendar open.
|
2026-03-14 08:55:21 +05:30
|
|
|
|
Set<DateTime> get _eventDates {
|
2026-03-30 10:05:23 +05:30
|
|
|
|
if (_cachedEventDates != null) return _cachedEventDates!;
|
2026-03-14 08:55:21 +05:30
|
|
|
|
final dates = <DateTime>{};
|
2026-03-30 10:05:23 +05:30
|
|
|
|
for (final e in _allEvents) {
|
2026-03-14 08:55:21 +05:30
|
|
|
|
try {
|
|
|
|
|
|
final start = DateTime.parse(e.startDate);
|
|
|
|
|
|
final end = DateTime.parse(e.endDate);
|
|
|
|
|
|
var d = DateTime(start.year, start.month, start.day);
|
|
|
|
|
|
final last = DateTime(end.year, end.month, end.day);
|
|
|
|
|
|
while (!d.isAfter(last)) {
|
|
|
|
|
|
dates.add(d);
|
|
|
|
|
|
d = d.add(const Duration(days: 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
2026-03-30 10:05:23 +05:30
|
|
|
|
_cachedEventDates = dates;
|
2026-03-14 08:55:21 +05:30
|
|
|
|
return dates;
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-04-04 16:51:30 +05:30
|
|
|
|
/// Show a calendar bottom sheet with TableCalendar for date filtering.
|
|
|
|
|
|
Future<DateTime?> _showCalendarBottomSheet() {
|
|
|
|
|
|
DateTime focusedDay = _selectedCustomDate ?? DateTime.now();
|
|
|
|
|
|
DateTime? selectedDay = _selectedCustomDate;
|
2026-03-14 08:55:21 +05:30
|
|
|
|
final eventDates = _eventDates;
|
|
|
|
|
|
|
2026-04-04 16:51:30 +05:30
|
|
|
|
return showModalBottomSheet<DateTime>(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
context: context,
|
2026-04-04 16:51:30 +05:30
|
|
|
|
isScrollControlled: true,
|
|
|
|
|
|
backgroundColor: Colors.transparent,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
builder: (ctx) {
|
2026-04-04 16:51:30 +05:30
|
|
|
|
return StatefulBuilder(builder: (ctx, setSheetState) {
|
|
|
|
|
|
final theme = Theme.of(ctx);
|
|
|
|
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
|
|
|
|
|
),
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
|
|
|
|
|
child: SafeArea(
|
|
|
|
|
|
top: false,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Drag handle
|
|
|
|
|
|
Container(
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 4,
|
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.grey[400],
|
|
|
|
|
|
borderRadius: BorderRadius.circular(2),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
2026-04-04 16:51:30 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
|
2026-04-04 16:51:30 +05:30
|
|
|
|
// TableCalendar
|
|
|
|
|
|
TableCalendar(
|
|
|
|
|
|
firstDay: DateTime(2020),
|
|
|
|
|
|
lastDay: DateTime(2030),
|
|
|
|
|
|
focusedDay: focusedDay,
|
|
|
|
|
|
selectedDayPredicate: (day) =>
|
|
|
|
|
|
selectedDay != null && isSameDay(day, selectedDay),
|
|
|
|
|
|
onDaySelected: (selected, focused) {
|
|
|
|
|
|
setSheetState(() {
|
|
|
|
|
|
selectedDay = selected;
|
|
|
|
|
|
focusedDay = focused;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
onPageChanged: (focused) {
|
|
|
|
|
|
setSheetState(() => focusedDay = focused);
|
|
|
|
|
|
},
|
|
|
|
|
|
eventLoader: (day) {
|
|
|
|
|
|
final normalized = DateTime(day.year, day.month, day.day);
|
|
|
|
|
|
return eventDates.contains(normalized) ? [true] : [];
|
|
|
|
|
|
},
|
|
|
|
|
|
startingDayOfWeek: StartingDayOfWeek.monday,
|
|
|
|
|
|
headerStyle: HeaderStyle(
|
|
|
|
|
|
formatButtonVisible: false,
|
|
|
|
|
|
titleCentered: true,
|
|
|
|
|
|
titleTextStyle: TextStyle(
|
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
color: isDark ? Colors.white : const Color(0xFF1A1A2E),
|
|
|
|
|
|
),
|
|
|
|
|
|
leftChevronIcon: Icon(Icons.chevron_left,
|
|
|
|
|
|
color: isDark ? Colors.white70 : const Color(0xFF374151)),
|
|
|
|
|
|
rightChevronIcon: Icon(Icons.chevron_right,
|
|
|
|
|
|
color: isDark ? Colors.white70 : const Color(0xFF374151)),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
2026-04-04 16:51:30 +05:30
|
|
|
|
calendarStyle: CalendarStyle(
|
|
|
|
|
|
selectedDecoration: const BoxDecoration(
|
|
|
|
|
|
color: Color(0xFF2563EB),
|
|
|
|
|
|
shape: BoxShape.circle,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
2026-04-04 16:51:30 +05:30
|
|
|
|
todayDecoration: BoxDecoration(
|
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
|
border: Border.all(color: const Color(0xFF2563EB), width: 1.5),
|
|
|
|
|
|
),
|
|
|
|
|
|
todayTextStyle: const TextStyle(
|
|
|
|
|
|
color: Color(0xFF2563EB),
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
defaultTextStyle: TextStyle(
|
|
|
|
|
|
color: isDark ? Colors.white : const Color(0xFF374151),
|
|
|
|
|
|
),
|
|
|
|
|
|
weekendTextStyle: TextStyle(
|
|
|
|
|
|
color: isDark ? Colors.white70 : const Color(0xFF374151),
|
|
|
|
|
|
),
|
|
|
|
|
|
outsideTextStyle: TextStyle(
|
|
|
|
|
|
color: isDark ? Colors.white24 : Colors.grey[400]!,
|
|
|
|
|
|
),
|
|
|
|
|
|
markerDecoration: const BoxDecoration(
|
|
|
|
|
|
color: Color(0xFFEF4444),
|
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
|
),
|
|
|
|
|
|
markerSize: 5,
|
|
|
|
|
|
markersMaxCount: 1,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
2026-04-04 16:51:30 +05:30
|
|
|
|
daysOfWeekStyle: DaysOfWeekStyle(
|
|
|
|
|
|
weekdayStyle: TextStyle(
|
|
|
|
|
|
color: isDark ? Colors.white54 : Colors.grey[500],
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
),
|
|
|
|
|
|
weekendStyle: TextStyle(
|
|
|
|
|
|
color: isDark ? Colors.white54 : Colors.grey[500],
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
|
|
// Done button
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
|
onPressed: () => Navigator.of(ctx).pop(selectedDay),
|
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
|
backgroundColor: const Color(0xFF2563EB),
|
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(24)),
|
|
|
|
|
|
elevation: 0,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Text('Done',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 16, fontWeight: FontWeight.w600)),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-01-31 15:23:18 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
Widget _buildHomeContent() {
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topCenter,
|
|
|
|
|
|
end: Alignment.bottomCenter,
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
Color(0xFF1A0A2E), // deep purple
|
|
|
|
|
|
Color(0xFF16213E), // dark navy
|
|
|
|
|
|
Color(0xFF0A0A0A), // near black
|
|
|
|
|
|
],
|
|
|
|
|
|
stops: [0.0, 0.4, 0.8],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: CustomScrollView(
|
|
|
|
|
|
slivers: [
|
|
|
|
|
|
// Hero section (dark bg)
|
|
|
|
|
|
SliverToBoxAdapter(child: _buildHeroSection()),
|
|
|
|
|
|
// White bottom section
|
|
|
|
|
|
SliverToBoxAdapter(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: _buildWhiteSection(),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-04-07 20:49:40 +05:30
|
|
|
|
/// Returns the image URL for a given event (for blurred bg).
|
|
|
|
|
|
String? _getEventImageUrl(EventModel event) {
|
|
|
|
|
|
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) return event.thumbImg;
|
|
|
|
|
|
if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) return event.images.first.image;
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
Widget _buildHeroSection() {
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return SafeArea(
|
|
|
|
|
|
bottom: false,
|
2026-04-07 20:49:40 +05:30
|
|
|
|
child: ValueListenableBuilder<int>(
|
|
|
|
|
|
valueListenable: _heroPageNotifier,
|
|
|
|
|
|
builder: (context, currentPage, _) {
|
|
|
|
|
|
final currentImg = _heroEvents.isNotEmpty ? _getEventImageUrl(_heroEvents[currentPage.clamp(0, _heroEvents.length - 1)]) : null;
|
|
|
|
|
|
return Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// ── Blurred background image layer ──
|
|
|
|
|
|
if (currentImg != null && currentImg.isNotEmpty)
|
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
|
child: ClipRect(
|
|
|
|
|
|
child: AnimatedSwitcher(
|
|
|
|
|
|
duration: const Duration(milliseconds: 500),
|
|
|
|
|
|
child: CachedNetworkImage(
|
|
|
|
|
|
key: ValueKey(currentImg),
|
|
|
|
|
|
imageUrl: currentImg,
|
|
|
|
|
|
memCacheWidth: 200,
|
|
|
|
|
|
memCacheHeight: 200,
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
placeholder: (_, __) => const SizedBox.shrink(),
|
|
|
|
|
|
errorWidget: (_, __, ___) => const SizedBox.shrink(),
|
|
|
|
|
|
imageBuilder: (context, imageProvider) => Stack(
|
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
ImageFiltered(
|
|
|
|
|
|
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
|
|
|
|
|
child: Image(
|
|
|
|
|
|
image: imageProvider,
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
Container(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.35),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
feat: Phase 1 critical gaps — gamification API, Razorpay checkout, Google OAuth, notifications
- Fix gamification endpoints to use Node.js server (app.eventifyplus.com)
- Replace 6 mock gamification methods with real API calls (dashboard, leaderboard, shop, redeem, submit)
- Add booking models, service, payment service (Razorpay), checkout provider
- Add 3-step CheckoutScreen with Razorpay native modal integration
- Add Google OAuth login (Flutter + Django backend)
- Add full notifications system (Django model + 3 endpoints + Flutter UI)
- Register CheckoutProvider, NotificationProvider in main.dart MultiProvider
- Wire notification bell in HomeScreen app bar
- Add razorpay_flutter ^1.3.7 and google_sign_in ^6.2.2 packages
2026-04-04 15:46:53 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-04-07 20:49:40 +05:30
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
|
2026-04-07 20:49:40 +05:30
|
|
|
|
// ── Foreground content ──
|
|
|
|
|
|
Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Top bar: location pill + search button
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: _openLocationSearch,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white.withOpacity(0.15),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(25),
|
|
|
|
|
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
|
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
_location.length > 20 ? '${_location.substring(0, 20)}...' : _location,
|
|
|
|
|
|
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-04-04 16:51:30 +05:30
|
|
|
|
),
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
),
|
2026-04-07 20:49:40 +05:30
|
|
|
|
Row(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const NotificationBell(),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: _openEventSearch,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 48,
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white.withOpacity(0.15),
|
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Icon(Icons.search, color: Colors.white, size: 24),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-04-07 20:49:40 +05:30
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
|
|
|
|
|
// Featured carousel
|
|
|
|
|
|
_heroEvents.isEmpty
|
|
|
|
|
|
? _loading
|
|
|
|
|
|
? const Padding(
|
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
|
height: 320,
|
|
|
|
|
|
child: _HeroShimmer(),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: const SizedBox(
|
|
|
|
|
|
height: 280,
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: Text('No events available',
|
|
|
|
|
|
style: TextStyle(color: Colors.white70)),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
RepaintBoundary(
|
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
|
behavior: HitTestBehavior.translucent,
|
|
|
|
|
|
onPanDown: (_) => _autoScrollTimer?.cancel(),
|
|
|
|
|
|
onPanEnd: (_) => _startAutoScroll(delay: const Duration(seconds: 3)),
|
|
|
|
|
|
onPanCancel: () => _startAutoScroll(delay: const Duration(seconds: 3)),
|
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
|
height: 320,
|
|
|
|
|
|
child: PageView.builder(
|
|
|
|
|
|
controller: _heroPageController,
|
|
|
|
|
|
onPageChanged: (page) {
|
|
|
|
|
|
_heroPageNotifier.value = page;
|
|
|
|
|
|
// 8s delay after manual swipe for full read time
|
|
|
|
|
|
_startAutoScroll(delay: const Duration(seconds: 8));
|
|
|
|
|
|
},
|
|
|
|
|
|
itemCount: _heroEvents.length,
|
|
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
|
|
// Scale animation: active card = 1.0, adjacent = 0.94
|
|
|
|
|
|
return AnimatedBuilder(
|
|
|
|
|
|
animation: _heroPageController,
|
|
|
|
|
|
builder: (context, child) {
|
|
|
|
|
|
double scale = index == _heroPageNotifier.value ? 1.0 : 0.94;
|
|
|
|
|
|
if (_heroPageController.position.haveDimensions) {
|
|
|
|
|
|
scale = (1.0 -
|
|
|
|
|
|
(_heroPageController.page! - index).abs() * 0.06)
|
|
|
|
|
|
.clamp(0.94, 1.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Transform.scale(scale: scale, child: child);
|
|
|
|
|
|
},
|
|
|
|
|
|
child: _buildHeroEventImage(_heroEvents[index]),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
// Pagination dots
|
|
|
|
|
|
_buildCarouselDots(),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
Widget _buildCarouselDots() {
|
2026-03-18 15:39:42 +05:30
|
|
|
|
return ValueListenableBuilder<int>(
|
|
|
|
|
|
valueListenable: _heroPageNotifier,
|
|
|
|
|
|
builder: (context, currentPage, _) {
|
|
|
|
|
|
return SizedBox(
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
height: 44,
|
2026-03-18 15:39:42 +05:30
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: List.generate(
|
|
|
|
|
|
_heroEvents.isEmpty ? 5 : _heroEvents.length,
|
|
|
|
|
|
(i) {
|
|
|
|
|
|
final isActive = i == currentPage;
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
if (_heroPageController.hasClients) {
|
|
|
|
|
|
_heroPageController.animateToPage(i,
|
|
|
|
|
|
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
child: SizedBox(
|
|
|
|
|
|
width: 44,
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: AnimatedContainer(
|
|
|
|
|
|
duration: const Duration(milliseconds: 200),
|
|
|
|
|
|
width: isActive ? 24 : 8,
|
|
|
|
|
|
height: 8,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: isActive
|
|
|
|
|
|
? Colors.white
|
|
|
|
|
|
: Colors.white.withValues(alpha: 0.4),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(4),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-18 15:39:42 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-03-14 08:55:21 +05:30
|
|
|
|
},
|
2026-03-18 15:39:42 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
2026-03-14 08:55:21 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
/// Build a hero image card with the image only (rounded),
|
|
|
|
|
|
/// and the title text placed below the image.
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Widget _buildHeroEventImage(EventModel event) {
|
|
|
|
|
|
String? img;
|
|
|
|
|
|
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) {
|
|
|
|
|
|
img = event.thumbImg;
|
|
|
|
|
|
} else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) {
|
|
|
|
|
|
img = event.images.first.image;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 12:03:13 +05:30
|
|
|
|
const double radius = 24.0;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
if (event.id != null) {
|
2026-04-04 18:45:19 +05:30
|
|
|
|
PostHogService.instance.capture('event_tapped', properties: {
|
|
|
|
|
|
'event_id': event.id,
|
|
|
|
|
|
'title': event.title ?? event.name ?? '',
|
|
|
|
|
|
'source': 'hero_carousel',
|
|
|
|
|
|
});
|
2026-03-19 12:03:13 +05:30
|
|
|
|
Navigator.push(context,
|
2026-04-07 20:49:40 +05:30
|
|
|
|
MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event, heroTag: 'event-hero-${event.id}-carousel')));
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Padding(
|
2026-03-19 12:03:13 +05:30
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
2026-04-04 17:49:37 +05:30
|
|
|
|
child: Hero(
|
2026-04-07 20:49:40 +05:30
|
|
|
|
tag: 'event-hero-${event.id}-carousel',
|
2026-04-04 17:49:37 +05:30
|
|
|
|
child: ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(radius),
|
|
|
|
|
|
child: Stack(
|
2026-03-19 12:03:13 +05:30
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// ── Layer 0: Event image (full-bleed) ──
|
|
|
|
|
|
img != null && img.isNotEmpty
|
|
|
|
|
|
? CachedNetworkImage(
|
|
|
|
|
|
imageUrl: img,
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
memCacheWidth: 700,
|
2026-03-30 10:05:23 +05:30
|
|
|
|
memCacheHeight: 400,
|
2026-03-19 12:03:13 +05:30
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
placeholder: (_, __) => const _HeroShimmer(radius: radius),
|
|
|
|
|
|
errorWidget: (_, __, ___) =>
|
|
|
|
|
|
Container(decoration: AppDecoration.blueGradientRounded(radius)),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Container(decoration: AppDecoration.blueGradientRounded(radius)),
|
|
|
|
|
|
|
|
|
|
|
|
// ── Layer 1: Bottom gradient overlay (text readability) ──
|
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
|
child: DecoratedBox(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topCenter,
|
|
|
|
|
|
end: Alignment.bottomCenter,
|
|
|
|
|
|
stops: const [0.35, 1.0],
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
Colors.transparent,
|
|
|
|
|
|
Colors.black.withOpacity(0.78),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
// ── Layer 2: Event type glassmorphism badge (top-left) ──
|
2026-03-19 12:03:13 +05:30
|
|
|
|
Positioned(
|
|
|
|
|
|
top: 14,
|
|
|
|
|
|
left: 14,
|
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
child: BackdropFilter(
|
|
|
|
|
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
|
|
|
|
decoration: BoxDecoration(
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
color: Colors.white.withValues(alpha: 0.18),
|
2026-03-19 12:03:13 +05:30
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.28)),
|
2026-03-19 12:03:13 +05:30
|
|
|
|
),
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
child: Row(
|
2026-03-19 12:03:13 +05:30
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
const Icon(Icons.star_rounded, color: Colors.amber, size: 13),
|
|
|
|
|
|
const SizedBox(width: 4),
|
2026-03-19 12:03:13 +05:30
|
|
|
|
Text(
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
_getEventTypeName(event),
|
|
|
|
|
|
style: const TextStyle(
|
2026-03-19 12:03:13 +05:30
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
letterSpacing: 0.6,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-19 12:03:13 +05:30
|
|
|
|
|
|
|
|
|
|
// ── Layer 3: Title + metadata (bottom overlay) ──
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
bottom: 18,
|
|
|
|
|
|
left: 16,
|
|
|
|
|
|
right: 16,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
event.title ?? event.name ?? '',
|
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
height: 1.25,
|
|
|
|
|
|
shadows: [
|
|
|
|
|
|
Shadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
if (event.startDate != null) ...[
|
|
|
|
|
|
const Icon(Icons.calendar_today_rounded,
|
|
|
|
|
|
color: Colors.white70, size: 12),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Text(
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
_formatDate(event.startDate!),
|
2026-03-19 12:03:13 +05:30
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
color: Colors.white70,
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
fontWeight: FontWeight.w500),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 10),
|
|
|
|
|
|
],
|
|
|
|
|
|
if (event.place != null && event.place!.isNotEmpty) ...[
|
|
|
|
|
|
const Icon(Icons.location_on_rounded,
|
|
|
|
|
|
color: Colors.white70, size: 12),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Flexible(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
event.place!,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
color: Colors.white70,
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
fontWeight: FontWeight.w500),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-04 17:49:37 +05:30
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
Widget _buildWhiteSection() {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 0),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Search bar
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: _openEventSearch,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: const Color(0xFFF3F4F6),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(28),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
child: const Text(
|
|
|
|
|
|
'Search events, artists or attractions',
|
|
|
|
|
|
style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 14),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
const SizedBox(height: 16),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
// Date filter chips
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
child: ListView(
|
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_dateChip(label: 'Date', icon: Icons.calendar_today, hasDropdown: true),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
_dateChip(label: 'Today'),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
_dateChip(label: 'Tomorrow'),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
_dateChip(label: 'This week'),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
|
|
|
|
|
// Top Events
|
|
|
|
|
|
const Text(
|
|
|
|
|
|
'Top Events',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: Color(0xFF111827),
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: 200,
|
2026-04-08 21:12:49 +05:30
|
|
|
|
child: _topEventsFiltered.isEmpty && _loading
|
2026-04-04 18:56:40 +05:30
|
|
|
|
? SingleChildScrollView(
|
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
|
child: Row(children: List.generate(3, (_) => const Padding(padding: EdgeInsets.only(right: 12), child: EventCardSkeleton()))),
|
|
|
|
|
|
)
|
2026-04-08 21:12:49 +05:30
|
|
|
|
: _topEventsFiltered.isEmpty
|
2026-03-14 08:55:21 +05:30
|
|
|
|
? Center(child: Text(
|
|
|
|
|
|
_selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found',
|
|
|
|
|
|
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
|
|
|
|
|
))
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
: PageView.builder(
|
|
|
|
|
|
controller: PageController(viewportFraction: 0.85),
|
|
|
|
|
|
physics: const PageScrollPhysics(),
|
2026-04-08 21:12:49 +05:30
|
|
|
|
itemCount: _topEventsFiltered.length,
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
itemBuilder: (context, index) => Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(right: 12),
|
2026-04-08 21:12:49 +05:30
|
|
|
|
child: _buildTopEventCard(_topEventsFiltered[index]),
|
feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
|
|
|
|
|
|
// Events Around You
|
|
|
|
|
|
Row(
|
2026-01-31 15:23:18 +05:30
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
|
children: [
|
2026-03-14 08:55:21 +05:30
|
|
|
|
const Text(
|
2026-01-31 15:23:18 +05:30
|
|
|
|
'Events Around You',
|
2026-03-14 08:55:21 +05:30
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: Color(0xFF111827),
|
|
|
|
|
|
fontSize: 20,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
TextButton(
|
2026-03-30 20:32:54 +05:30
|
|
|
|
onPressed: () => setState(() => _categoriesExpanded = !_categoriesExpanded),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
_categoriesExpanded ? 'Show Less' : 'View All',
|
|
|
|
|
|
style: const TextStyle(color: Color(0xFF2563EB), fontWeight: FontWeight.w600),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
const SizedBox(height: 12),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-30 20:32:54 +05:30
|
|
|
|
// Category chips — horizontal scroll (collapsed) or wrap grid (expanded)
|
|
|
|
|
|
AnimatedCrossFade(
|
|
|
|
|
|
duration: const Duration(milliseconds: 300),
|
|
|
|
|
|
crossFadeState: _categoriesExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
|
|
|
|
|
// Collapsed: horizontal scroll
|
|
|
|
|
|
firstChild: SizedBox(
|
|
|
|
|
|
height: 140,
|
|
|
|
|
|
child: ListView(
|
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_categoryChip(
|
|
|
|
|
|
label: 'All Events',
|
|
|
|
|
|
icon: Icons.grid_view_rounded,
|
|
|
|
|
|
selected: _selectedTypeId == -1,
|
|
|
|
|
|
onTap: () => _onSelectType(-1),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
|
for (final t in _types) ...[
|
|
|
|
|
|
_categoryChip(
|
|
|
|
|
|
label: t.name,
|
|
|
|
|
|
imageUrl: t.iconUrl,
|
|
|
|
|
|
icon: _getIconForType(t.name),
|
|
|
|
|
|
selected: _selectedTypeId == t.id,
|
|
|
|
|
|
onTap: () => _onSelectType(t.id),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Expanded: wrap grid showing all categories
|
|
|
|
|
|
secondChild: Wrap(
|
|
|
|
|
|
spacing: 10,
|
|
|
|
|
|
runSpacing: 10,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
children: [
|
2026-01-31 15:23:18 +05:30
|
|
|
|
_categoryChip(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
label: 'All Events',
|
|
|
|
|
|
icon: Icons.grid_view_rounded,
|
|
|
|
|
|
selected: _selectedTypeId == -1,
|
|
|
|
|
|
onTap: () => _onSelectType(-1),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-30 20:32:54 +05:30
|
|
|
|
for (final t in _types)
|
2026-03-14 08:55:21 +05:30
|
|
|
|
_categoryChip(
|
|
|
|
|
|
label: t.name,
|
|
|
|
|
|
imageUrl: t.iconUrl,
|
|
|
|
|
|
icon: _getIconForType(t.name),
|
|
|
|
|
|
selected: _selectedTypeId == t.id,
|
|
|
|
|
|
onTap: () => _onSelectType(t.id),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
const SizedBox(height: 16),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-30 20:32:54 +05:30
|
|
|
|
// Event sections — shelves (All) or filtered list (specific category)
|
2026-03-30 10:05:23 +05:30
|
|
|
|
if (_loading)
|
2026-04-04 16:51:30 +05:30
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
|
|
|
|
|
child: Column(children: List.generate(3, (_) => const Padding(padding: EdgeInsets.only(bottom: 8), child: EventListSkeleton()))),
|
2026-03-30 10:05:23 +05:30
|
|
|
|
)
|
|
|
|
|
|
else if (_allFilteredByDate.isEmpty && _selectedDateFilter.isNotEmpty)
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(40),
|
|
|
|
|
|
child: Center(child: Text(
|
|
|
|
|
|
'No events for "$_selectedDateFilter"',
|
|
|
|
|
|
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
|
|
|
|
|
)),
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
2026-03-30 20:32:54 +05:30
|
|
|
|
AnimatedSwitcher(
|
|
|
|
|
|
duration: const Duration(milliseconds: 300),
|
|
|
|
|
|
switchInCurve: Curves.easeOut,
|
|
|
|
|
|
switchOutCurve: Curves.easeIn,
|
|
|
|
|
|
child: _selectedTypeId == -1
|
|
|
|
|
|
// "All Events" — show category shelf sections
|
|
|
|
|
|
? Column(
|
|
|
|
|
|
key: const ValueKey('shelves'),
|
|
|
|
|
|
children: [
|
|
|
|
|
|
for (final t in _types)
|
|
|
|
|
|
if (_allFilteredByDate.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
|
|
|
|
|
|
_buildTypeSection(t),
|
|
|
|
|
|
const SizedBox(height: 18),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
// Specific category — show vertical list of events for that category only
|
|
|
|
|
|
: Column(
|
|
|
|
|
|
key: ValueKey('filtered_$_selectedTypeId'),
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Category name header
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(bottom: 14),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
_types.firstWhere((t) => t.id == _selectedTypeId, orElse: () => _types.first).name,
|
|
|
|
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF111827)),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'${_allFilteredByDate.where((e) => e.eventTypeId == _selectedTypeId).length} events',
|
|
|
|
|
|
style: const TextStyle(fontSize: 13, color: Color(0xFF9CA3AF)),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Event cards
|
|
|
|
|
|
for (final e in _allFilteredByDate.where((e) => e.eventTypeId == _selectedTypeId))
|
|
|
|
|
|
_buildFullWidthCard(e),
|
|
|
|
|
|
if (_allFilteredByDate.where((e) => e.eventTypeId == _selectedTypeId).isEmpty)
|
|
|
|
|
|
const Padding(
|
|
|
|
|
|
padding: EdgeInsets.all(40),
|
|
|
|
|
|
child: Center(child: Text('No events in this category', style: TextStyle(color: Color(0xFF9CA3AF)))),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-30 10:05:23 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
|
|
|
|
|
|
// Bottom padding for nav bar
|
|
|
|
|
|
const SizedBox(height: 100),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
],
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _dateChip({required String label, IconData? icon, bool hasDropdown = false}) {
|
|
|
|
|
|
final isSelected = _selectedDateFilter == label;
|
|
|
|
|
|
// For 'Date' chip, show the picked date instead of just "Date"
|
|
|
|
|
|
String displayLabel = label;
|
|
|
|
|
|
if (label == 'Date' && isSelected && _selectedCustomDate != null) {
|
|
|
|
|
|
final d = _selectedCustomDate!;
|
|
|
|
|
|
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
|
|
|
|
displayLabel = '${d.day} ${months[d.month - 1]}';
|
|
|
|
|
|
}
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () => _onDateChipTap(label),
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: isSelected ? const Color(0xFF2563EB) : const Color(0xFFF3F4F6),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
border: Border.all(color: isSelected ? const Color(0xFF2563EB) : const Color(0xFFE5E7EB)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
if (icon != null) ...[
|
|
|
|
|
|
Icon(icon, size: 16, color: isSelected ? Colors.white : const Color(0xFF374151)),
|
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
|
],
|
|
|
|
|
|
Text(
|
|
|
|
|
|
displayLabel,
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: isSelected ? Colors.white : const Color(0xFF374151),
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (hasDropdown) ...[
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Icon(Icons.keyboard_arrow_down, size: 16, color: isSelected ? Colors.white : const Color(0xFF374151)),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildTopEventCard(EventModel event) {
|
|
|
|
|
|
String? img;
|
|
|
|
|
|
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) {
|
|
|
|
|
|
img = event.thumbImg;
|
|
|
|
|
|
} else if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) {
|
|
|
|
|
|
img = event.images.first.image;
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-14 08:55:21 +05:30
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
if (event.id != null) {
|
2026-04-07 20:49:40 +05:30
|
|
|
|
Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event, heroTag: 'event-hero-${event.id}-top')));
|
2026-03-14 08:55:21 +05:30
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-04 17:49:37 +05:30
|
|
|
|
child: Hero(
|
2026-04-07 20:49:40 +05:30
|
|
|
|
tag: 'event-hero-${event.id}-top',
|
2026-04-04 17:49:37 +05:30
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 150,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
),
|
|
|
|
|
|
clipBehavior: Clip.antiAlias,
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
fit: StackFit.expand,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
children: [
|
|
|
|
|
|
// Background image
|
|
|
|
|
|
img != null && img.isNotEmpty
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
? CachedNetworkImage(
|
|
|
|
|
|
imageUrl: img,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 300,
|
2026-03-30 10:05:23 +05:30
|
|
|
|
memCacheHeight: 200,
|
2026-03-14 08:55:21 +05:30
|
|
|
|
fit: BoxFit.cover,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
color: const Color(0xFF374151),
|
|
|
|
|
|
child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white38))),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
2026-03-14 08:55:21 +05:30
|
|
|
|
color: const Color(0xFF374151),
|
|
|
|
|
|
child: const Icon(Icons.image, color: Colors.white38, size: 40),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Container(
|
|
|
|
|
|
color: const Color(0xFF374151),
|
|
|
|
|
|
child: const Icon(Icons.image, color: Colors.white38, size: 40),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Dark gradient overlay at bottom
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
bottom: 0,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topCenter,
|
|
|
|
|
|
end: Alignment.bottomCenter,
|
|
|
|
|
|
colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Title text at bottom left
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 12,
|
|
|
|
|
|
right: 12,
|
|
|
|
|
|
bottom: 12,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
event.title ?? event.name ?? '',
|
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
height: 1.2,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-04-04 17:49:37 +05:30
|
|
|
|
),
|
2026-03-14 08:55:21 +05:30
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
/// Build a type section that follows your requested layout rules:
|
|
|
|
|
|
/// - If type has <= 5 events => single horizontal row of compact cards.
|
|
|
|
|
|
/// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns).
|
|
|
|
|
|
Widget _buildTypeSection(EventTypeModel type) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
2026-03-30 10:05:23 +05:30
|
|
|
|
final eventsForType = _allFilteredByDate.where((e) => e.eventTypeId == type.id).toList();
|
2026-03-11 20:13:13 +05:30
|
|
|
|
final n = eventsForType.length;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
// Header row
|
|
|
|
|
|
Widget header = Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(type.name, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
_onSelectType(type.id);
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Text('View All', style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
// If <= 5 events: show one horizontal row using _buildHorizontalEventCard
|
|
|
|
|
|
if (n <= 5) {
|
|
|
|
|
|
return Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
header,
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: 290, // card height: image 180 + text ~110
|
|
|
|
|
|
child: ListView.separated(
|
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
|
|
|
|
itemBuilder: (ctx, idx) => _buildHorizontalEventCard(eventsForType[idx]),
|
|
|
|
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
|
|
|
|
itemCount: eventsForType.length,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
|
|
|
|
|
// For 6+ events: arrange into columns where each column has up to 3 stacked cards.
|
|
|
|
|
|
final columnsCount = (n / 3).ceil();
|
|
|
|
|
|
final columnWidth = 260.0; // narrower so second column peeks in
|
|
|
|
|
|
final verticalCardHeight = 120.0; // each stacked card height matches sample
|
|
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
header,
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
|
|
|
|
|
|
// Container height must accommodate 3 stacked cards + small gaps
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: (verticalCardHeight * 3) + 16, // 3 cards + spacing
|
|
|
|
|
|
child: ListView.separated(
|
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
|
|
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
|
|
|
|
itemBuilder: (ctx, colIndex) {
|
|
|
|
|
|
// Build one column: contains up to 3 items: indices colIndex*3 + 0/1/2
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
width: columnWidth,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// top card
|
|
|
|
|
|
if ((colIndex * 3 + 0) < n)
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: verticalCardHeight,
|
|
|
|
|
|
child: _buildStackedCard(eventsForType[colIndex * 3 + 0]),
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
|
|
|
|
|
const SizedBox(height: 0),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
// middle card
|
|
|
|
|
|
if ((colIndex * 3 + 1) < n)
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: verticalCardHeight,
|
|
|
|
|
|
child: _buildStackedCard(eventsForType[colIndex * 3 + 1]),
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
|
|
|
|
|
const SizedBox(height: 0),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
// bottom card
|
|
|
|
|
|
if ((colIndex * 3 + 2) < n)
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: verticalCardHeight,
|
|
|
|
|
|
child: _buildStackedCard(eventsForType[colIndex * 3 + 2]),
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
|
|
|
|
|
const SizedBox(height: 0),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
itemCount: columnsCount,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
/// A stacked card styled to match your sample (left square thumbnail, bold title).
|
|
|
|
|
|
/// REMOVED: price/rating row (per your request).
|
|
|
|
|
|
Widget _buildStackedCard(EventModel e) {
|
2026-01-31 15:23:18 +05:30
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
String? img;
|
|
|
|
|
|
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
|
|
|
|
|
|
img = e.thumbImg;
|
|
|
|
|
|
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
|
|
|
|
|
|
img = e.images.first.image;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
2026-03-29 19:25:40 +05:30
|
|
|
|
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id, initialEvent: e)));
|
2026-01-31 15:23:18 +05:30
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
margin: const EdgeInsets.symmetric(vertical: 0),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12, offset: const Offset(0, 8))],
|
|
|
|
|
|
),
|
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// thumbnail square (rounded)
|
|
|
|
|
|
ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
|
child: img != null && img.isNotEmpty
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
? CachedNetworkImage(
|
|
|
|
|
|
imageUrl: img,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 192,
|
2026-03-30 10:05:23 +05:30
|
|
|
|
memCacheHeight: 192,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
width: 96,
|
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
placeholder: (_, __) => Container(width: 96, color: theme.dividerColor, child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(width: 96, color: theme.dividerColor),
|
|
|
|
|
|
)
|
2026-03-11 20:13:13 +05:30
|
|
|
|
: Container(width: 96, height: double.infinity, color: theme.dividerColor),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
|
|
|
|
Text(e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 18)),
|
|
|
|
|
|
// removed price/rating row here per request
|
|
|
|
|
|
]),
|
|
|
|
|
|
),
|
|
|
|
|
|
// optional heart icon aligned top-right
|
|
|
|
|
|
Icon(Icons.favorite_border, color: theme.hintColor),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Compact card used inside the one-row layout for small counts (<=5).
|
|
|
|
|
|
/// Matches Figma: vertical card with image, date badge, title, location, "Free".
|
|
|
|
|
|
Widget _buildHorizontalEventCard(EventModel e) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
String? img;
|
|
|
|
|
|
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
|
|
|
|
|
|
img = e.thumbImg;
|
|
|
|
|
|
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
|
|
|
|
|
|
img = e.images.first.image;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse day & month for the date badge
|
|
|
|
|
|
String day = '';
|
|
|
|
|
|
String month = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
final parts = e.startDate.split('-');
|
|
|
|
|
|
if (parts.length == 3) {
|
|
|
|
|
|
day = int.parse(parts[2]).toString();
|
|
|
|
|
|
const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
|
|
|
|
|
|
month = months[int.parse(parts[1]) - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
|
|
final venue = e.venueName ?? e.place ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
2026-03-29 19:25:40 +05:30
|
|
|
|
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id, initialEvent: e)));
|
2026-03-11 20:13:13 +05:30
|
|
|
|
},
|
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Image with date badge
|
|
|
|
|
|
Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
|
child: img != null && img.isNotEmpty
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
? CachedNetworkImage(
|
|
|
|
|
|
imageUrl: img,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 440,
|
|
|
|
|
|
memCacheHeight: 360,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
width: 220,
|
|
|
|
|
|
height: 180,
|
|
|
|
|
|
fit: BoxFit.cover,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
height: 180,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
width: 220,
|
|
|
|
|
|
height: 180,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Icon(Icons.image, size: 40, color: theme.hintColor),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Container(
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
height: 180,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(18),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Icon(Icons.image, size: 40, color: theme.hintColor),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Date badge
|
|
|
|
|
|
if (day.isNotEmpty)
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
top: 10,
|
|
|
|
|
|
right: 10,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.08),
|
|
|
|
|
|
blurRadius: 6,
|
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
day,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
color: Colors.black87,
|
|
|
|
|
|
height: 1.1,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
month,
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: Colors.black54,
|
|
|
|
|
|
height: 1.2,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
// Title
|
|
|
|
|
|
Text(
|
|
|
|
|
|
e.title ?? e.name ?? '',
|
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (venue.isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(height: 4),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Text(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
venue,
|
|
|
|
|
|
maxLines: 1,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
],
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'Free',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Format a date string (YYYY-MM-DD) to short display like "4 Mar".
|
|
|
|
|
|
String _formatDateShort(String dateStr) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final parts = dateStr.split('-');
|
|
|
|
|
|
if (parts.length == 3) {
|
|
|
|
|
|
final day = int.parse(parts[2]);
|
|
|
|
|
|
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
|
|
|
|
final month = months[int.parse(parts[1]) - 1];
|
|
|
|
|
|
return '$day $month';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
return dateStr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Full width card used when a single type is selected (vertical list).
|
|
|
|
|
|
/// Matches Figma: large image, badge, title, date + venue.
|
|
|
|
|
|
Widget _buildFullWidthCard(EventModel e) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
String? img;
|
|
|
|
|
|
if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
|
|
|
|
|
|
img = e.thumbImg;
|
|
|
|
|
|
} else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
|
|
|
|
|
|
img = e.images.first.image;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build date range string
|
|
|
|
|
|
final startShort = _formatDateShort(e.startDate);
|
|
|
|
|
|
final endShort = _formatDateShort(e.endDate);
|
|
|
|
|
|
final dateRange = startShort == endShort ? startShort : '$startShort - $endShort';
|
|
|
|
|
|
|
|
|
|
|
|
final venue = e.venueName ?? e.place ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
2026-03-29 19:25:40 +05:30
|
|
|
|
if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id, initialEvent: e)));
|
2026-03-11 20:13:13 +05:30
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 18),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(color: theme.shadowColor.withOpacity(0.10), blurRadius: 16, offset: const Offset(0, 6)),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Image with badge
|
|
|
|
|
|
Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
ClipRRect(
|
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
|
child: img != null && img.isNotEmpty
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
? CachedNetworkImage(
|
|
|
|
|
|
imageUrl: img,
|
perf: add memCacheWidth/memCacheHeight to all thumbnail images
All CachedNetworkImage instances in list/card contexts now decode at
2x rendered size instead of full resolution. A 3000x2000 event photo
previously decoded to ~24MB in GPU memory even when shown at 96px —
now decodes to <1MB.
Affected screens (16 CachedNetworkImage instances total):
- home_screen.dart: hero (800w), top card (300w), stacked (192w),
horizontal (440x360), full-width (800x400), search (112x112),
filter sheet (160x160), type icons (112x112)
- home_desktop_screen.dart: mini (128x128), grid (600x280), horiz (600x296)
- calendar_screen.dart: event card (400x300)
- profile_screen.dart: avatar (size*2), event tile (120x120)
learn_more_screen.dart intentionally unchanged — full-res for detail view.
Estimated memory reduction: ~500MB → ~30MB for a typical home screen.
2026-03-20 22:26:52 +05:30
|
|
|
|
memCacheWidth: 800,
|
|
|
|
|
|
memCacheHeight: 400,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 200,
|
|
|
|
|
|
fit: BoxFit.cover,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 200,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Center(child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2))),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 200,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Icon(Icons.image, size: 48, color: theme.hintColor),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 200,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Icon(Icons.image, size: 48, color: theme.hintColor),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// "ADDED BY EVENTIFY" badge
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
top: 14,
|
|
|
|
|
|
left: 14,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: const [
|
|
|
|
|
|
Icon(Icons.star, color: Colors.white, size: 14),
|
|
|
|
|
|
SizedBox(width: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'ADDED BY EVENTIFY',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
letterSpacing: 0.5,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
// Title + date/venue
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
e.title ?? e.name ?? '',
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
fontSize: 17,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
maxLines: 2,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(Icons.calendar_today_outlined, size: 14, color: theme.hintColor),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
dateRange,
|
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (venue.isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
|
Icon(Icons.location_on_outlined, size: 14, color: theme.hintColor),
|
|
|
|
|
|
const SizedBox(width: 3),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
venue,
|
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
),
|
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
IconData _getIconForType(String typeName) {
|
|
|
|
|
|
final name = typeName.toLowerCase();
|
|
|
|
|
|
if (name.contains('music')) return Icons.music_note;
|
|
|
|
|
|
if (name.contains('art') || name.contains('comedy')) return Icons.palette;
|
|
|
|
|
|
if (name.contains('festival')) return Icons.celebration;
|
|
|
|
|
|
if (name.contains('heritage') || name.contains('history')) return Icons.account_balance;
|
|
|
|
|
|
if (name.contains('sport')) return Icons.sports;
|
|
|
|
|
|
if (name.contains('food')) return Icons.restaurant;
|
|
|
|
|
|
return Icons.event;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 10:05:23 +05:30
|
|
|
|
void _onSelectType(int id) {
|
2026-03-11 20:13:13 +05:30
|
|
|
|
setState(() {
|
|
|
|
|
|
_selectedTypeId = id;
|
2026-03-30 10:05:23 +05:30
|
|
|
|
_events = id == -1 ? List.from(_allEvents) : _allEvents.where((e) => e.eventTypeId == id).toList();
|
|
|
|
|
|
_cachedFilteredEvents = null;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
String _getShortEmailLabel() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final parts = _username.split('@');
|
|
|
|
|
|
if (parts.isNotEmpty && parts[0].isNotEmpty) return parts[0];
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
return 'You';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-19 12:03:13 +05:30
|
|
|
|
|
|
|
|
|
|
/// Animated shimmer placeholder shown while a hero card image is loading.
|
|
|
|
|
|
/// Renders a blue-toned scan-line effect matching the app's colour palette.
|
|
|
|
|
|
class _HeroShimmer extends StatefulWidget {
|
|
|
|
|
|
final double radius;
|
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>
2026-03-21 13:28:19 +05:30
|
|
|
|
const _HeroShimmer({this.radius = 24.0});
|
2026-03-19 12:03:13 +05:30
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<_HeroShimmer> createState() => _HeroShimmerState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _HeroShimmerState extends State<_HeroShimmer>
|
|
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
|
late final AnimationController _ctrl;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
_ctrl = AnimationController(
|
|
|
|
|
|
vsync: this,
|
|
|
|
|
|
duration: const Duration(milliseconds: 1400),
|
|
|
|
|
|
)..repeat();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_ctrl.dispose();
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return AnimatedBuilder(
|
|
|
|
|
|
animation: _ctrl,
|
|
|
|
|
|
builder: (_, __) {
|
|
|
|
|
|
final x = -1.5 + _ctrl.value * 3.0;
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(widget.radius),
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment(x - 1.0, 0),
|
|
|
|
|
|
end: Alignment(x, 0),
|
|
|
|
|
|
colors: const [
|
|
|
|
|
|
Color(0xFF1A2A4A),
|
|
|
|
|
|
Color(0xFF2D4580),
|
|
|
|
|
|
Color(0xFF1A2A4A),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|