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
173 lines
5.3 KiB
Dart
173 lines
5.3 KiB
Dart
// lib/features/events/models/event_models.dart
|
|
import '../../../core/api/api_endpoints.dart';
|
|
|
|
class EventTypeModel {
|
|
final int id;
|
|
final String name;
|
|
final String? 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) {
|
|
return EventTypeModel(
|
|
id: j['id'] as int,
|
|
name: (j['event_type'] ?? j['name'] ?? '') as String,
|
|
iconUrl: _resolveMediaUrl((j['event_type_icon'] ?? j['icon_url']) as String?),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EventImageModel {
|
|
final bool isPrimary;
|
|
final String image;
|
|
|
|
EventImageModel({required this.isPrimary, required this.image});
|
|
|
|
factory EventImageModel.fromJson(Map<String, dynamic> j) {
|
|
return EventImageModel(
|
|
isPrimary: j['is_primary'] == true,
|
|
image: EventTypeModel._resolveMediaUrl(j['image'] as String?) ?? '',
|
|
);
|
|
}
|
|
}
|
|
|
|
class EventModel {
|
|
final int id;
|
|
final String name;
|
|
final String? title;
|
|
final String? description;
|
|
final String startDate; // YYYY-MM-DD
|
|
final String endDate;
|
|
final String? startTime;
|
|
final String? endTime;
|
|
final String? pincode;
|
|
final String? place;
|
|
final bool isBookable;
|
|
final int? eventTypeId;
|
|
final String? thumbImg;
|
|
final List<EventImageModel> images;
|
|
|
|
// NEW fields mapped from backend
|
|
final String? importantInformation;
|
|
final String? venueName;
|
|
final String? eventStatus;
|
|
final String? cancelledReason;
|
|
|
|
// Geo / location fields
|
|
final double? latitude;
|
|
final double? longitude;
|
|
final String? locationName;
|
|
|
|
// Structured important info list [{title, value}, ...]
|
|
final List<Map<String, String>> importantInfo;
|
|
|
|
// Review stats (populated when backend includes them)
|
|
final double? averageRating;
|
|
final int? reviewCount;
|
|
|
|
// Contributor fields (EVT-001)
|
|
final String? contributorId;
|
|
final String? contributorName;
|
|
final String? contributorTier;
|
|
|
|
EventModel({
|
|
required this.id,
|
|
required this.name,
|
|
this.title,
|
|
this.description,
|
|
required this.startDate,
|
|
required this.endDate,
|
|
this.startTime,
|
|
this.endTime,
|
|
this.pincode,
|
|
this.place,
|
|
this.isBookable = true,
|
|
this.eventTypeId,
|
|
this.thumbImg,
|
|
this.images = const [],
|
|
this.importantInformation,
|
|
this.venueName,
|
|
this.eventStatus,
|
|
this.cancelledReason,
|
|
this.latitude,
|
|
this.longitude,
|
|
this.locationName,
|
|
this.importantInfo = const [],
|
|
this.averageRating,
|
|
this.reviewCount,
|
|
this.contributorId,
|
|
this.contributorName,
|
|
this.contributorTier,
|
|
});
|
|
|
|
/// Safely parse a double from backend (may arrive as String or num)
|
|
static double? _parseDouble(dynamic raw) {
|
|
if (raw == null) return null;
|
|
if (raw is num) return raw.toDouble();
|
|
if (raw is String) return double.tryParse(raw);
|
|
return null;
|
|
}
|
|
|
|
/// Safely parse important_info from backend (list of {title, value} maps)
|
|
static List<Map<String, String>> _parseImportantInfo(dynamic raw) {
|
|
if (raw is List) {
|
|
return raw.map<Map<String, String>>((e) {
|
|
if (e is Map) {
|
|
return {
|
|
'title': (e['title'] ?? '').toString(),
|
|
'value': (e['value'] ?? '').toString(),
|
|
};
|
|
}
|
|
return {'title': '', 'value': e.toString()};
|
|
}).toList();
|
|
}
|
|
return [];
|
|
}
|
|
|
|
factory EventModel.fromJson(Map<String, dynamic> j) {
|
|
final imgs = <EventImageModel>[];
|
|
if (j['images'] is List) {
|
|
for (final im in j['images']) {
|
|
if (im is Map<String, dynamic>) imgs.add(EventImageModel.fromJson(im));
|
|
}
|
|
}
|
|
|
|
return EventModel(
|
|
id: j['id'] is int ? j['id'] as int : int.parse(j['id'].toString()),
|
|
name: (j['name'] ?? '') as String,
|
|
title: j['title'] as String?,
|
|
description: j['description'] as String?,
|
|
startDate: (j['start_date'] ?? '') as String,
|
|
endDate: (j['end_date'] ?? '') as String,
|
|
startTime: j['start_time'] as String?,
|
|
endTime: j['end_time'] as String?,
|
|
pincode: j['pincode'] 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'),
|
|
eventTypeId: j['event_type'] is int ? j['event_type'] as int : (j['event_type'] != null ? int.tryParse(j['event_type'].toString()) : null),
|
|
thumbImg: EventTypeModel._resolveMediaUrl(j['thumb_img'] as String?),
|
|
images: imgs,
|
|
importantInformation: j['important_information'] as String?,
|
|
venueName: j['venue_name'] as String?,
|
|
eventStatus: j['event_status'] as String?,
|
|
cancelledReason: j['cancelled_reason'] as String?,
|
|
latitude: _parseDouble(j['latitude']),
|
|
longitude: _parseDouble(j['longitude']),
|
|
locationName: j['location_name'] as String?,
|
|
importantInfo: _parseImportantInfo(j['important_info']),
|
|
averageRating: (j['average_rating'] as num?)?.toDouble(),
|
|
reviewCount: (j['review_count'] as num?)?.toInt(),
|
|
contributorId: j['contributor_id']?.toString(),
|
|
contributorName: j['contributor_name'] as String?,
|
|
contributorTier: j['contributor_tier'] as String?,
|
|
);
|
|
}
|
|
}
|