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:
2026-04-08 21:12:49 +05:30
parent 479fe5e119
commit c40e600937
7 changed files with 190 additions and 9 deletions

View File

@@ -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]),
),
),
),