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:
@@ -60,6 +60,18 @@ class AuthService {
|
||||
// Save phone if provided (optional)
|
||||
if (res['phone_number'] != null) await prefs.setString('phone_number', res['phone_number'].toString());
|
||||
|
||||
// Save profile photo from login response
|
||||
final rawPhoto = res['profile_photo']?.toString() ?? '';
|
||||
if (rawPhoto.isNotEmpty) {
|
||||
final photoUrl = rawPhoto.startsWith('http') ? rawPhoto : 'https://em.eventifyplus.com$rawPhoto';
|
||||
await prefs.setString('profileImage_$savedEmail', photoUrl);
|
||||
await prefs.setString('profileImage', photoUrl);
|
||||
}
|
||||
|
||||
// Save Eventify ID
|
||||
final eventifyId = res['eventify_id']?.toString() ?? '';
|
||||
if (eventifyId.isNotEmpty) await prefs.setString('eventify_id', eventifyId);
|
||||
|
||||
PostHogService.instance.identify(savedEmail, properties: {
|
||||
'username': displayCandidate,
|
||||
'login_method': 'email',
|
||||
@@ -178,6 +190,18 @@ class AuthService {
|
||||
}
|
||||
if (res['phone_number'] != null) await prefs.setString('phone_number', res['phone_number'].toString());
|
||||
|
||||
// Save profile photo from Google login response
|
||||
final rawPhoto = res['profile_photo']?.toString() ?? '';
|
||||
if (rawPhoto.isNotEmpty) {
|
||||
final photoUrl = rawPhoto.startsWith('http') ? rawPhoto : 'https://em.eventifyplus.com$rawPhoto';
|
||||
await prefs.setString('profileImage_$serverEmail', photoUrl);
|
||||
await prefs.setString('profileImage', photoUrl);
|
||||
}
|
||||
|
||||
// Save Eventify ID
|
||||
final eventifyId = res['eventify_id']?.toString() ?? '';
|
||||
if (eventifyId.isNotEmpty) await prefs.setString('eventify_id', eventifyId);
|
||||
|
||||
PostHogService.instance.identify(serverEmail, properties: {
|
||||
'username': displayName,
|
||||
'login_method': 'google',
|
||||
|
||||
@@ -77,6 +77,10 @@ class EventModel {
|
||||
final String? contributorName;
|
||||
final String? contributorTier;
|
||||
|
||||
// Curation flags
|
||||
final bool isFeatured;
|
||||
final bool isTopEvent;
|
||||
|
||||
EventModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
@@ -105,6 +109,8 @@ class EventModel {
|
||||
this.contributorId,
|
||||
this.contributorName,
|
||||
this.contributorTier,
|
||||
this.isFeatured = false,
|
||||
this.isTopEvent = false,
|
||||
});
|
||||
|
||||
/// Safely parse a double from backend (may arrive as String or num)
|
||||
@@ -167,6 +173,8 @@ class EventModel {
|
||||
contributorId: j['contributor_id']?.toString(),
|
||||
contributorName: j['contributor_name'] as String?,
|
||||
contributorTier: j['contributor_tier'] as String?,
|
||||
isFeatured: j['is_featured'] == true || j['is_featured']?.toString().toLowerCase() == 'true',
|
||||
isTopEvent: j['is_top_event'] == true || j['is_top_event']?.toString().toLowerCase() == 'true',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,32 @@ class EventsService {
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Featured events for the home screen hero carousel.
|
||||
Future<List<EventModel>> getFeaturedEvents() async {
|
||||
final res = await _api.post(ApiEndpoints.featuredEvents, requiresAuth: false);
|
||||
final events = res['events'] ?? res['data'] ?? [];
|
||||
if (events is List) {
|
||||
return events
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((e) => EventModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Top events for the home screen top events section.
|
||||
Future<List<EventModel>> getTopEvents() async {
|
||||
final res = await _api.post(ApiEndpoints.topEvents, requiresAuth: false);
|
||||
final events = res['events'] ?? res['data'] ?? [];
|
||||
if (events is List) {
|
||||
return events
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((e) => EventModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
||||
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
|
||||
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
|
||||
|
||||
@@ -152,11 +152,28 @@ class GamificationService {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Submit Contribution
|
||||
// POST /v1/gamification/submit-event body: event data
|
||||
// 1. Upload each image to /v1/upload/file → get back { url, fileId, ... }
|
||||
// 2. POST /v1/gamification/submit-event with `media` (uploaded objects)
|
||||
// ---------------------------------------------------------------------------
|
||||
Future<void> submitContribution(Map<String, dynamic> data) async {
|
||||
final email = await _getUserEmail();
|
||||
final body = <String, dynamic>{'user_id': email, ...data};
|
||||
|
||||
// Upload images if present
|
||||
final rawPaths = (data['images'] as List?)?.cast<String>() ?? [];
|
||||
final List<Map<String, dynamic>> uploadedMedia = [];
|
||||
|
||||
for (final path in rawPaths) {
|
||||
final result = await _api.uploadFile(ApiEndpoints.uploadFile, path);
|
||||
uploadedMedia.add(result);
|
||||
}
|
||||
|
||||
// Build submission body — use `media` (server canonical field)
|
||||
final body = <String, dynamic>{
|
||||
'user_id': email,
|
||||
...Map.from(data)..remove('images'),
|
||||
if (uploadedMedia.isNotEmpty) 'media': uploadedMedia,
|
||||
};
|
||||
|
||||
await _api.post(
|
||||
ApiEndpoints.contributeSubmit,
|
||||
body: body,
|
||||
|
||||
Reference in New Issue
Block a user