feat(contribute): upload event images to OneDrive before submission
- ApiClient.uploadFile() — multipart POST to /v1/upload/file (60s timeout)
- ApiEndpoints.uploadFile — points to Node.js upload endpoint
- GamificationService.submitContribution() now uploads each picked image
to OneDrive via the server upload pipeline, then passes the returned
{ fileId, url, ... } objects as `media` in the submission body
(replaces broken behaviour of sending local device paths as strings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
// backend-driven
|
||||
List<EventModel> _allEvents = []; // master copy, never filtered
|
||||
List<EventModel> _events = [];
|
||||
List<EventModel> _featuredEvents = [];
|
||||
List<EventModel> _topEventsList = [];
|
||||
List<EventTypeModel> _types = [];
|
||||
int _selectedTypeId = -1; // -1 == All
|
||||
bool _categoriesExpanded = false;
|
||||
@@ -62,6 +64,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
super.initState();
|
||||
_heroPageNotifier = ValueNotifier(0);
|
||||
_loadUserDataAndEvents();
|
||||
_loadCuratedEvents();
|
||||
_startAutoScroll();
|
||||
PostHogService.instance.screen('Home');
|
||||
}
|
||||
@@ -112,7 +115,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
_userLng = prefs.getDouble('user_lng');
|
||||
|
||||
try {
|
||||
// Fetch types and events in parallel for faster loading.
|
||||
// Fetch types and location-based events in parallel.
|
||||
// Prefer haversine (lat/lng) when GPS coords are available; fall back to pincode.
|
||||
final results = await Future.wait([
|
||||
_events_service_getEventTypesSafe(),
|
||||
@@ -189,6 +192,24 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
await _loadUserDataAndEvents();
|
||||
}
|
||||
@@ -583,8 +604,51 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
|
||||
// Get hero events (first 4 events for the carousel)
|
||||
List<EventModel> get _heroEvents => _events.take(6).toList();
|
||||
// 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();
|
||||
}
|
||||
|
||||
String _formatDate(String dateStr) {
|
||||
try {
|
||||
@@ -1641,12 +1705,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: _allFilteredByDate.isEmpty && _loading
|
||||
child: _topEventsFiltered.isEmpty && _loading
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: List.generate(3, (_) => const Padding(padding: EdgeInsets.only(right: 12), child: EventCardSkeleton()))),
|
||||
)
|
||||
: _allFilteredByDate.isEmpty
|
||||
: _topEventsFiltered.isEmpty
|
||||
? Center(child: Text(
|
||||
_selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found',
|
||||
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
||||
@@ -1654,10 +1718,10 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
: PageView.builder(
|
||||
controller: PageController(viewportFraction: 0.85),
|
||||
physics: const PageScrollPhysics(),
|
||||
itemCount: _allFilteredByDate.length,
|
||||
itemCount: _topEventsFiltered.length,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: _buildTopEventCard(_allFilteredByDate[index]),
|
||||
child: _buildTopEventCard(_topEventsFiltered[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user