perf: optimize loading time — paginated API, slim payloads, local category filtering

Backend: Rewrote EventListAPI to query per-type with DB-level LIMIT
instead of loading all 734 events into memory. Added slim serializer
(32KB vs 154KB). Added DB indexes on event_type_id and pincode.

Frontend: Category chips now filter locally from _allEvents (instant,
no API call). Top Events and category sections always show all types
regardless of selected category. Added TTL caching for event types
(30min) and events (5min). Reduced API timeout from 30s to 10s.
Added memCacheHeight to all CachedNetworkImage widgets. Batched
setState calls from 5 to 2 during startup. Cached _eventDates getter.

Switched baseUrl to em.eventifyplus.com (Django via Nginx+SSL).
Added initialEvent param to LearnMoreScreen for instant detail views.
Resolved relative media URLs for category icons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 10:05:23 +05:30
parent c32f343558
commit a7f3b215e4
8 changed files with 357 additions and 82 deletions

View File

@@ -1,11 +1,10 @@
{ {
"version": "0.0.1", "version": "0.0.1",
"autoPort": true,
"configurations": [ "configurations": [
{ {
"name": "flutter-web", "name": "flutter-web",
"runtimeExecutable": "bash", "runtimeExecutable": "flutter",
"runtimeArgs": ["/Users/bshtechnologies/Documents/Eventify-frontend/run_web.sh"], "runtimeArgs": ["run", "-d", "chrome", "--web-port", "8080", "--web-browser-flag", "--disable-web-security"],
"port": 8080 "port": 8080
} }
] ]

View File

@@ -5,9 +5,9 @@ import 'package:http/http.dart' as http;
import '../storage/token_storage.dart'; import '../storage/token_storage.dart';
class ApiClient { class ApiClient {
static const Duration _timeout = Duration(seconds: 30); static const Duration _timeout = Duration(seconds: 10);
// Set to true to enable mock/offline development mode (useful when backend is unavailable) // Set to true to enable mock/offline development mode (useful when backend is unavailable)
static const bool _developmentMode = true; static const bool _developmentMode = false;
/// POST request /// POST request
/// ///
@@ -57,6 +57,39 @@ class ApiClient {
'email': email, 'email': email,
'phone_number': finalBody['phone_number'] ?? '+1234567890', 'phone_number': finalBody['phone_number'] ?? '+1234567890',
}; };
} else if (url.contains('/events/type-list/')) {
if (kDebugMode) debugPrint('Development mode: returning mock event types');
return {
'event_types': [
{'id': 1, 'event_type': 'Concert', 'event_type_icon': null},
{'id': 2, 'event_type': 'Workshop', 'event_type_icon': null},
{'id': 3, 'event_type': 'Festival', 'event_type_icon': null},
{'id': 4, 'event_type': 'Sports', 'event_type_icon': null},
{'id': 5, 'event_type': 'Conference', 'event_type_icon': null},
{'id': 6, 'event_type': 'Exhibition', 'event_type_icon': null},
],
};
} else if (url.contains('/events/pincode-events/')) {
if (kDebugMode) debugPrint('Development mode: returning mock events');
return {'events': _mockEvents};
} else if (url.contains('/events/event-details/')) {
if (kDebugMode) debugPrint('Development mode: returning mock event detail');
final eventId = finalBody['event_id'] ?? 1;
final match = _mockEvents.where((e) => e['id'] == eventId);
return match.isNotEmpty
? Map<String, dynamic>.from(match.first)
: Map<String, dynamic>.from(_mockEvents.first);
} else if (url.contains('/events/events-by-month-year/')) {
if (kDebugMode) debugPrint('Development mode: returning mock calendar');
return {
'total_number_of_events': 3,
'dates': ['2026-04-05', '2026-04-12', '2026-04-20'],
'date_events': [
{'date': '2026-04-05', 'count': 1},
{'date': '2026-04-12', 'count': 2},
{'date': '2026-04-20', 'count': 1},
],
};
} }
} }
@@ -103,6 +136,153 @@ class ApiClient {
return _handleResponse(url, response, finalParams); return _handleResponse(url, response, finalParams);
} }
// ---------------------------------------------------------------------------
// Mock event data for development / offline mode
// ---------------------------------------------------------------------------
static final List<Map<String, dynamic>> _mockEvents = [
{
'id': 1,
'name': 'Tech Innovation Summit 2026',
'title': 'Tech Innovation Summit',
'description':
'Join industry leaders for a two-day summit exploring the latest breakthroughs in AI, cloud computing, and sustainable technology. Featuring keynote speakers, hands-on workshops, and networking sessions.',
'start_date': '2026-04-15',
'end_date': '2026-04-16',
'start_time': '09:00',
'end_time': '18:00',
'pincode': '560001',
'place': 'Bengaluru International Exhibition Centre',
'is_bookable': true,
'event_type': 5,
'thumb_img': 'https://picsum.photos/seed/event1/600/400',
'images': [
{'is_primary': true, 'image': 'https://picsum.photos/seed/event1a/800/500'},
{'is_primary': false, 'image': 'https://picsum.photos/seed/event1b/800/500'},
],
'important_information': 'Please carry a valid photo ID for entry.',
'venue_name': 'BIEC Hall 2',
'event_status': 'active',
'latitude': 13.0147,
'longitude': 77.5636,
'location_name': 'Bengaluru',
'important_info': [
{'title': 'Entry', 'value': 'Free with registration'},
{'title': 'Parking', 'value': 'Available on-site'},
],
},
{
'id': 2,
'name': 'Sunset Music Festival',
'title': 'Sunset Music Festival',
'description':
'An open-air music festival featuring live performances from top artists across genres. Enjoy food stalls, art installations, and an unforgettable sunset experience.',
'start_date': '2026-04-20',
'end_date': '2026-04-20',
'start_time': '16:00',
'end_time': '23:00',
'pincode': '400001',
'place': 'Marine Drive Amphitheatre',
'is_bookable': true,
'event_type': 1,
'thumb_img': 'https://picsum.photos/seed/event2/600/400',
'images': [
{'is_primary': true, 'image': 'https://picsum.photos/seed/event2a/800/500'},
],
'venue_name': 'Marine Drive Amphitheatre',
'event_status': 'active',
'latitude': 18.9432,
'longitude': 72.8235,
'location_name': 'Mumbai',
'important_info': [
{'title': 'Age Limit', 'value': '16+'},
],
},
{
'id': 3,
'name': 'Creative Design Workshop',
'title': 'Hands-on Design Workshop',
'description':
'A full-day workshop on UI/UX design principles, prototyping in Figma, and building design systems. Perfect for beginners and intermediate designers.',
'start_date': '2026-05-03',
'end_date': '2026-05-03',
'start_time': '10:00',
'end_time': '17:00',
'pincode': '110001',
'place': 'Design Hub Co-working',
'is_bookable': true,
'event_type': 2,
'thumb_img': 'https://picsum.photos/seed/event3/600/400',
'images': [
{'is_primary': true, 'image': 'https://picsum.photos/seed/event3a/800/500'},
],
'venue_name': 'Design Hub',
'event_status': 'active',
'latitude': 28.6139,
'longitude': 77.2090,
'location_name': 'New Delhi',
'important_info': [
{'title': 'Bring', 'value': 'Laptop with Figma installed'},
{'title': 'Seats', 'value': '30 max'},
],
},
{
'id': 4,
'name': 'Marathon for a Cause',
'title': 'City Marathon 2026',
'description':
'Run for fitness, run for charity! Choose from 5K, 10K, or full marathon routes through the city. All proceeds support local education initiatives.',
'start_date': '2026-04-12',
'end_date': '2026-04-12',
'start_time': '05:30',
'end_time': '12:00',
'pincode': '600001',
'place': 'Marina Beach Road',
'is_bookable': true,
'event_type': 4,
'thumb_img': 'https://picsum.photos/seed/event4/600/400',
'images': [
{'is_primary': true, 'image': 'https://picsum.photos/seed/event4a/800/500'},
],
'venue_name': 'Marina Beach',
'event_status': 'active',
'latitude': 13.0500,
'longitude': 80.2824,
'location_name': 'Chennai',
'important_info': [
{'title': 'Registration', 'value': 'Closes April 10'},
],
},
{
'id': 5,
'name': 'Art & Culture Exhibition',
'title': 'Contemporary Art Exhibition',
'description':
'Explore contemporary artworks from emerging and established artists. The exhibition features paintings, sculptures, and digital art installations.',
'start_date': '2026-05-10',
'end_date': '2026-05-15',
'start_time': '11:00',
'end_time': '20:00',
'pincode': '500001',
'place': 'Salar Jung Museum Grounds',
'is_bookable': true,
'event_type': 6,
'thumb_img': 'https://picsum.photos/seed/event5/600/400',
'images': [
{'is_primary': true, 'image': 'https://picsum.photos/seed/event5a/800/500'},
{'is_primary': false, 'image': 'https://picsum.photos/seed/event5b/800/500'},
],
'venue_name': 'Salar Jung Museum',
'event_status': 'active',
'latitude': 17.3713,
'longitude': 78.4804,
'location_name': 'Hyderabad',
'important_info': [
{'title': 'Entry Fee', 'value': '₹200'},
{'title': 'Photography', 'value': 'Allowed without flash'},
],
},
];
/// Build request body and attach token + username if available /// Build request body and attach token + username if available
Future<Map<String, dynamic>> _buildAuthBody(Map<String, dynamic>? body, bool requiresAuth) async { Future<Map<String, dynamic>> _buildAuthBody(Map<String, dynamic>? body, bool requiresAuth) async {
final Map<String, dynamic> finalBody = {}; final Map<String, dynamic> finalBody = {};

View File

@@ -3,7 +3,11 @@ class ApiEndpoints {
// Change this to your desired backend base URL (local or UAT) // Change this to your desired backend base URL (local or UAT)
// For local Django dev use: "http://127.0.0.1:8000/api" // For local Django dev use: "http://127.0.0.1:8000/api"
// For UAT: "https://uat.eventifyplus.com/api" // For UAT: "https://uat.eventifyplus.com/api"
static const String baseUrl = "https://uat.eventifyplus.com/api"; static const String baseUrl = "https://em.eventifyplus.com/api";
/// Base URL for media files (images, icons uploaded via Django admin).
/// Relative paths like `/media/...` are resolved against this.
static const String mediaBaseUrl = "https://em.eventifyplus.com";
// Auth // Auth
static const String register = "$baseUrl/user/register/"; static const String register = "$baseUrl/user/register/";

View File

@@ -1,4 +1,6 @@
// lib/features/events/models/event_models.dart // lib/features/events/models/event_models.dart
import '../../../core/api/api_endpoints.dart';
class EventTypeModel { class EventTypeModel {
final int id; final int id;
final String name; final String name;
@@ -6,11 +8,18 @@ class EventTypeModel {
EventTypeModel({required this.id, required this.name, this.iconUrl}); EventTypeModel({required this.id, required this.name, this.iconUrl});
/// Resolve a relative media path (e.g. `/media/...`) to a full URL.
static String? _resolveMediaUrl(String? raw) {
if (raw == null || raw.isEmpty) return null;
if (raw.startsWith('http://') || raw.startsWith('https://')) return raw;
return '${ApiEndpoints.mediaBaseUrl}$raw';
}
factory EventTypeModel.fromJson(Map<String, dynamic> j) { factory EventTypeModel.fromJson(Map<String, dynamic> j) {
return EventTypeModel( return EventTypeModel(
id: j['id'] as int, id: j['id'] as int,
name: (j['event_type'] ?? j['name'] ?? '') as String, name: (j['event_type'] ?? j['name'] ?? '') as String,
iconUrl: (j['event_type_icon'] ?? j['icon_url']) as String?, iconUrl: _resolveMediaUrl((j['event_type_icon'] ?? j['icon_url']) as String?),
); );
} }
} }
@@ -24,7 +33,7 @@ class EventImageModel {
factory EventImageModel.fromJson(Map<String, dynamic> j) { factory EventImageModel.fromJson(Map<String, dynamic> j) {
return EventImageModel( return EventImageModel(
isPrimary: j['is_primary'] == true, isPrimary: j['is_primary'] == true,
image: (j['image'] ?? '') as String, image: EventTypeModel._resolveMediaUrl(j['image'] as String?) ?? '',
); );
} }
} }
@@ -129,7 +138,7 @@ class EventModel {
place: (j['place'] ?? j['venue_name']) as String?, place: (j['place'] ?? j['venue_name']) as String?,
isBookable: j['is_bookable'] == null ? true : (j['is_bookable'] == true || j['is_bookable'].toString().toLowerCase() == 'true'), isBookable: j['is_bookable'] == null ? true : (j['is_bookable'] == true || j['is_bookable'].toString().toLowerCase() == 'true'),
eventTypeId: j['event_type'] is int ? j['event_type'] as int : (j['event_type'] != null ? int.tryParse(j['event_type'].toString()) : null), eventTypeId: j['event_type'] is int ? j['event_type'] as int : (j['event_type'] != null ? int.tryParse(j['event_type'].toString()) : null),
thumbImg: j['thumb_img'] as String?, thumbImg: EventTypeModel._resolveMediaUrl(j['thumb_img'] as String?),
images: imgs, images: imgs,
importantInformation: j['important_information'] as String?, importantInformation: j['important_information'] as String?,
venueName: j['venue_name'] as String?, venueName: j['venue_name'] as String?,

View File

@@ -1,5 +1,4 @@
// lib/features/events/services/events_service.dart // lib/features/events/services/events_service.dart
import 'package:intl/intl.dart';
import '../../../core/api/api_client.dart'; import '../../../core/api/api_client.dart';
import '../../../core/api/api_endpoints.dart'; import '../../../core/api/api_endpoints.dart';
import '../models/event_models.dart'; import '../models/event_models.dart';
@@ -7,27 +6,62 @@ import '../models/event_models.dart';
class EventsService { class EventsService {
final ApiClient _api = ApiClient(); final ApiClient _api = ApiClient();
// ---------------------------------------------------------------------------
// In-memory caches with TTL
// ---------------------------------------------------------------------------
static List<EventTypeModel>? _cachedTypes;
static DateTime? _typesCacheTime;
static const _typesCacheTTL = Duration(minutes: 30);
static List<EventModel>? _cachedAllEvents;
static DateTime? _eventsCacheTime;
static const _eventsCacheTTL = Duration(minutes: 5);
/// Get event types (POST to /events/type-list/) /// Get event types (POST to /events/type-list/)
/// Cached for 30 minutes since event types rarely change.
Future<List<EventTypeModel>> getEventTypes() async { Future<List<EventTypeModel>> getEventTypes() async {
if (_cachedTypes != null &&
_typesCacheTime != null &&
DateTime.now().difference(_typesCacheTime!) < _typesCacheTTL) {
return _cachedTypes!;
}
final res = await _api.post(ApiEndpoints.eventTypes, requiresAuth: false); final res = await _api.post(ApiEndpoints.eventTypes, requiresAuth: false);
final list = <EventTypeModel>[]; final list = <EventTypeModel>[];
final data = res['event_types'] ?? res['event_types'] ?? res; final data = res['event_types'] ?? res;
if (data is List) { if (data is List) {
for (final e in data) { for (final e in data) {
if (e is Map<String, dynamic>) list.add(EventTypeModel.fromJson(e)); if (e is Map<String, dynamic>) list.add(EventTypeModel.fromJson(e));
} }
} else if (res['event_types'] is List) {
for (final e in res['event_types']) {
list.add(EventTypeModel.fromJson(Map<String, dynamic>.from(e)));
}
} }
_cachedTypes = list;
_typesCacheTime = DateTime.now();
return list; return list;
} }
/// Get events filtered by pincode (POST to /events/pincode-events/) /// Get events filtered by pincode with pagination.
/// Use pincode='all' to fetch all events. /// [page] starts at 1. [pageSize] defaults to 50.
Future<List<EventModel>> getEventsByPincode(String pincode) async { /// Returns a list of events for the requested page.
final res = await _api.post(ApiEndpoints.eventsByPincode, body: {'pincode': pincode}, requiresAuth: false); Future<List<EventModel>> getEventsByPincode(String pincode, {int page = 1, int pageSize = 50, int perType = 5}) async {
// Use cache for 'all' pincode queries (first page only for initial load)
if (pincode == 'all' &&
page == 1 &&
_cachedAllEvents != null &&
_eventsCacheTime != null &&
DateTime.now().difference(_eventsCacheTime!) < _eventsCacheTTL) {
return _cachedAllEvents!;
}
final Map<String, dynamic> body = {'pincode': pincode, 'page': page, 'page_size': pageSize};
// Diverse mode: fetch a few events per type so all categories are represented
if (perType > 0 && page == 1) body['per_type'] = perType;
final res = await _api.post(
ApiEndpoints.eventsByPincode,
body: body,
requiresAuth: false,
);
final list = <EventModel>[]; final list = <EventModel>[];
final events = res['events'] ?? res['data'] ?? []; final events = res['events'] ?? res['data'] ?? [];
if (events is List) { if (events is List) {
@@ -35,6 +69,11 @@ class EventsService {
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e))); if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
} }
} }
if (pincode == 'all' && page == 1) {
_cachedAllEvents = list;
_eventsCacheTime = DateTime.now();
}
return list; return list;
} }
@@ -45,23 +84,21 @@ class EventsService {
} }
/// Events by month and year for calendar (POST to /events/events-by-month-year/) /// Events by month and year for calendar (POST to /events/events-by-month-year/)
/// Accepts month string and year int.
/// Returns Map with 'dates' (list of YYYY-MM-DD) and 'date_events' (list with counts).
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async { Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false); final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
// expected keys: dates, total_number_of_events, date_events
return res; return res;
} }
/// Convenience: get events for a specific date (YYYY-MM-DD) /// Convenience: get events for a specific date (YYYY-MM-DD).
/// Uses the cached events list when available to avoid redundant API calls.
Future<List<EventModel>> getEventsForDate(String date) async { Future<List<EventModel>> getEventsForDate(String date) async {
// Simplest approach: hit pincode-events with filter or hit events-by-month-year and then
// query event-details for events of that date. Assuming backend doesn't provide direct endpoint,
// we'll call eventsByPincode('all') and filter locally by date — acceptable for demo/small datasets.
final all = await getEventsByPincode('all'); final all = await getEventsByPincode('all');
return all.where((e) { return all.where((e) {
try { try {
return e.startDate == date || e.endDate == date || (DateTime.parse(e.startDate).isBefore(DateTime.parse(date)) && DateTime.parse(e.endDate).isAfter(DateTime.parse(date))); return e.startDate == date ||
e.endDate == date ||
(DateTime.parse(e.startDate).isBefore(DateTime.parse(date)) &&
DateTime.parse(e.endDate).isAfter(DateTime.parse(date)));
} catch (_) { } catch (_) {
return false; return false;
} }

View File

@@ -319,6 +319,7 @@ class _HomeContentState extends State<_HomeContent>
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
memCacheWidth: 1400, memCacheWidth: 1400,
memCacheHeight: 800,
placeholder: (_, __) => Container( placeholder: (_, __) => Container(
color: const Color(0xFF0A0E1A), color: const Color(0xFF0A0E1A),
), ),
@@ -527,6 +528,7 @@ class _HomeContentState extends State<_HomeContent>
imageUrl: img, imageUrl: img,
fit: BoxFit.cover, fit: BoxFit.cover,
memCacheWidth: 1400, memCacheWidth: 1400,
memCacheHeight: 800,
) )
else else
Container(color: const Color(0xFF0A0E1A)), Container(color: const Color(0xFF0A0E1A)),

View File

@@ -36,6 +36,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
final EventsService _eventsService = EventsService(); final EventsService _eventsService = EventsService();
// backend-driven // backend-driven
List<EventModel> _allEvents = []; // master copy, never filtered
List<EventModel> _events = []; List<EventModel> _events = [];
List<EventTypeModel> _types = []; List<EventTypeModel> _types = [];
int _selectedTypeId = -1; // -1 == All int _selectedTypeId = -1; // -1 == All
@@ -87,7 +88,6 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
final coordMatch = RegExp(r'^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$').firstMatch(storedLocation); final coordMatch = RegExp(r'^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$').firstMatch(storedLocation);
if (coordMatch != null) { if (coordMatch != null) {
_location = 'Current Location'; _location = 'Current Location';
setState(() {});
// Reverse geocode in background to get actual place name // Reverse geocode in background to get actual place name
_reverseGeocodeAndSave( _reverseGeocodeAndSave(
double.parse(coordMatch.group(1)!), double.parse(coordMatch.group(1)!),
@@ -110,17 +110,19 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
if (mounted) { if (mounted) {
setState(() { setState(() {
_types = types; _types = types;
_allEvents = events;
_events = events; _events = events;
_selectedTypeId = -1; _selectedTypeId = -1;
_cachedFilteredEvents = null; // invalidate cache _cachedFilteredEvents = null;
_cachedEventDates = null;
_loading = false;
}); });
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
} }
} finally {
if (mounted) setState(() => _loading = false);
} }
} }
@@ -545,6 +547,52 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
List<EventModel>? _cachedFilteredEvents; List<EventModel>? _cachedFilteredEvents;
String _cachedFilterKey = ''; String _cachedFilterKey = '';
// 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();
}
/// Returns the subset of [_events] that match the active date-filter chip. /// Returns the subset of [_events] that match the active date-filter chip.
/// Uses caching to avoid re-parsing dates on every access. /// Uses caching to avoid re-parsing dates on every access.
List<EventModel> get _filteredEvents { List<EventModel> get _filteredEvents {
@@ -887,9 +935,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
} }
/// Collect all event dates (start + end range) to show dots on the calendar. /// Collect all event dates (start + end range) to show dots on the calendar.
/// Cached to avoid re-parsing on every calendar open.
Set<DateTime> get _eventDates { Set<DateTime> get _eventDates {
if (_cachedEventDates != null) return _cachedEventDates!;
final dates = <DateTime>{}; final dates = <DateTime>{};
for (final e in _events) { for (final e in _allEvents) {
try { try {
final start = DateTime.parse(e.startDate); final start = DateTime.parse(e.startDate);
final end = DateTime.parse(e.endDate); final end = DateTime.parse(e.endDate);
@@ -901,6 +951,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
} }
} catch (_) {} } catch (_) {}
} }
_cachedEventDates = dates;
return dates; return dates;
} }
@@ -1318,6 +1369,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: img, imageUrl: img,
memCacheWidth: 700, memCacheWidth: 700,
memCacheHeight: 400,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: (_, __) => const _HeroShimmer(radius: radius), placeholder: (_, __) => const _HeroShimmer(radius: radius),
errorWidget: (_, __, ___) => errorWidget: (_, __, ___) =>
@@ -1498,18 +1550,18 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox( SizedBox(
height: 200, height: 200,
child: _filteredEvents.isEmpty && _loading child: _allFilteredByDate.isEmpty && _loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _filteredEvents.isEmpty : _allFilteredByDate.isEmpty
? Center(child: Text( ? Center(child: Text(
_selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found', _selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found',
style: const TextStyle(color: Color(0xFF9CA3AF)), style: const TextStyle(color: Color(0xFF9CA3AF)),
)) ))
: ListView.separated( : ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: _filteredEvents.length, itemCount: _allFilteredByDate.length,
separatorBuilder: (_, __) => const SizedBox(width: 12), separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) => _buildTopEventCard(_filteredEvents[index]), itemBuilder: (context, index) => _buildTopEventCard(_allFilteredByDate[index]),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -1565,14 +1617,13 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Event sections by type // Event sections by type — always show ALL categories
if (_selectedTypeId == -1) ...[
if (_loading) if (_loading)
const Padding( const Padding(
padding: EdgeInsets.all(40), padding: EdgeInsets.all(40),
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
) )
else if (_filteredEvents.isEmpty && _selectedDateFilter.isNotEmpty) else if (_allFilteredByDate.isEmpty && _selectedDateFilter.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.all(40), padding: const EdgeInsets.all(40),
child: Center(child: Text( child: Center(child: Text(
@@ -1584,23 +1635,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
Column( Column(
children: [ children: [
for (final t in _types) for (final t in _types)
if (_filteredEvents.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[ if (_allFilteredByDate.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
_buildTypeSection(t), _buildTypeSection(t),
const SizedBox(height: 18), const SizedBox(height: 18),
], ],
], ],
), ),
] else ...[
if (_loading)
const Padding(
padding: EdgeInsets.all(40),
child: Center(child: CircularProgressIndicator()),
)
else
Column(
children: _filteredEvents.map((e) => _buildFullWidthCard(e)).toList(),
),
],
// Bottom padding for nav bar // Bottom padding for nav bar
const SizedBox(height: 100), const SizedBox(height: 100),
@@ -1680,6 +1720,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: img, imageUrl: img,
memCacheWidth: 300, memCacheWidth: 300,
memCacheHeight: 200,
fit: BoxFit.cover, fit: BoxFit.cover,
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
@@ -1740,7 +1781,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
/// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns). /// - 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) { Widget _buildTypeSection(EventTypeModel type) {
final theme = Theme.of(context); final theme = Theme.of(context);
final eventsForType = _filteredEvents.where((e) => e.eventTypeId == type.id).toList(); final eventsForType = _allFilteredByDate.where((e) => e.eventTypeId == type.id).toList();
final n = eventsForType.length; final n = eventsForType.length;
// Header row // Header row
@@ -1874,6 +1915,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: img, imageUrl: img,
memCacheWidth: 192, memCacheWidth: 192,
memCacheHeight: 192,
width: 96, width: 96,
height: double.infinity, height: double.infinity,
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -2241,18 +2283,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
return Icons.event; return Icons.event;
} }
void _onSelectType(int id) async { void _onSelectType(int id) {
setState(() { setState(() {
_selectedTypeId = id; _selectedTypeId = id;
_events = id == -1 ? List.from(_allEvents) : _allEvents.where((e) => e.eventTypeId == id).toList();
_cachedFilteredEvents = null;
}); });
try {
final all = await _eventsService.getEventsByPincode(_pincode);
final filtered = id == -1 ? all : all.where((e) => e.eventTypeId == id).toList();
if (mounted) setState(() { _events = filtered; _cachedFilteredEvents = null; });
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
} }
String _getShortEmailLabel() { String _getShortEmailLabel() {

View File

@@ -265,6 +265,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
CachedNetworkImage( CachedNetworkImage(
imageUrl: heroImage, imageUrl: heroImage,
fit: BoxFit.cover, fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
placeholder: (_, __) => Container( placeholder: (_, __) => Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@@ -471,6 +473,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
CachedNetworkImage( CachedNetworkImage(
imageUrl: images[i], imageUrl: images[i],
fit: BoxFit.cover, fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
placeholder: (_, __) => Container(color: theme.dividerColor), placeholder: (_, __) => Container(color: theme.dividerColor),
errorWidget: (_, __, ___) => Container( errorWidget: (_, __, ___) => Container(
color: theme.dividerColor, color: theme.dividerColor,
@@ -724,6 +728,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
builder: (context, currentPage, _) => CachedNetworkImage( builder: (context, currentPage, _) => CachedNetworkImage(
imageUrl: images[currentPage], imageUrl: images[currentPage],
fit: BoxFit.cover, fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
placeholder: (_, __) => Container( placeholder: (_, __) => Container(
@@ -782,6 +788,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
itemBuilder: (_, i) => CachedNetworkImage( itemBuilder: (_, i) => CachedNetworkImage(
imageUrl: images[i], imageUrl: images[i],
fit: BoxFit.cover, fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
width: double.infinity, width: double.infinity,
placeholder: (_, __) => Container( placeholder: (_, __) => Container(
color: theme.dividerColor, color: theme.dividerColor,