2026-01-31 15:23:18 +05:30
|
|
|
|
// lib/screens/learn_more_screen.dart
|
2026-03-11 20:13:13 +05:30
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
import 'dart:ui';
|
2026-03-14 08:57:25 +05:30
|
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
import 'package:flutter/material.dart';
|
2026-03-11 20:13:13 +05:30
|
|
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
2026-03-30 20:09:50 +05:30
|
|
|
|
// google_maps_flutter removed — using OpenStreetMap static map preview instead
|
2026-03-11 20:13:13 +05:30
|
|
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
import '../features/events/models/event_models.dart';
|
|
|
|
|
|
import '../features/events/services/events_service.dart';
|
2026-03-21 07:29:55 +05:30
|
|
|
|
import '../core/auth/auth_guard.dart';
|
security: sanitize all error messages shown to users
Created centralized userFriendlyError() utility that converts raw
exceptions into clean, user-friendly messages. Strips hostnames,
ports, OS error codes, HTTP status codes, stack traces, and Django
field names. Maps network/timeout/auth/server errors to plain
English messages.
Fixed 16 locations across 10 files:
- home_screen, calendar_screen, learn_more_screen (SnackBar/Text)
- login_screen, desktop_login_screen (SnackBar)
- profile_screen, contribute_screen, search_screen (SnackBar)
- review_form, review_section (inline error text)
- gamification_provider (error field)
Also removed double-wrapped exceptions in ReviewService (rethrow
instead of throw Exception('Failed to...: $e')).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:15:02 +05:30
|
|
|
|
import '../core/utils/error_utils.dart';
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
import '../core/constants.dart';
|
feat: add complete review/rating system for events
New feature: Users can view, submit, and interact with event reviews.
Components added:
- ReviewModel, ReviewStatsModel, ReviewListResponse (models)
- ReviewService with getReviews, submitReview, markHelpful, flagReview
- StarRatingInput (interactive 5-star picker with labels)
- StarDisplay (read-only fractional star display)
- ReviewSummary (average rating + distribution bars)
- ReviewForm (star picker + comment field + submit/update)
- ReviewCard (avatar, timestamp, expandable comment, helpful/flag)
- ReviewSection (main container with pagination and state mgmt)
Integration:
- Added to LearnMoreScreen (both mobile and desktop layouts)
- Review API endpoints point to app.eventifyplus.com Node.js backend
- EventModel updated with averageRating/reviewCount fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:04:37 +05:30
|
|
|
|
import '../features/reviews/widgets/review_section.dart';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
class LearnMoreScreen extends StatefulWidget {
|
|
|
|
|
|
final int eventId;
|
2026-03-29 19:25:40 +05:30
|
|
|
|
final EventModel? initialEvent;
|
|
|
|
|
|
const LearnMoreScreen({Key? key, required this.eventId, this.initialEvent}) : super(key: key);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<LearnMoreScreen> createState() => _LearnMoreScreenState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|
|
|
|
|
final EventsService _service = EventsService();
|
|
|
|
|
|
|
|
|
|
|
|
bool _loading = true;
|
|
|
|
|
|
EventModel? _event;
|
|
|
|
|
|
String? _error;
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
// Carousel
|
|
|
|
|
|
final PageController _pageController = PageController();
|
2026-03-18 17:00:25 +05:30
|
|
|
|
late final ValueNotifier<int> _pageNotifier;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
Timer? _autoScrollTimer;
|
|
|
|
|
|
|
|
|
|
|
|
// About section
|
|
|
|
|
|
bool _aboutExpanded = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Wishlist (UI-only)
|
|
|
|
|
|
bool _wishlisted = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Google Map
|
|
|
|
|
|
GoogleMapController? _mapController;
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
2026-03-18 17:00:25 +05:30
|
|
|
|
_pageNotifier = ValueNotifier(0);
|
2026-03-29 19:25:40 +05:30
|
|
|
|
if (widget.initialEvent != null) {
|
|
|
|
|
|
_event = widget.initialEvent;
|
|
|
|
|
|
_loading = false;
|
2026-03-30 20:20:15 +05:30
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
|
_startAutoScroll();
|
|
|
|
|
|
// Fetch full event details in background to get important_information, images, etc.
|
|
|
|
|
|
_loadFullDetails();
|
|
|
|
|
|
});
|
2026-03-29 19:25:40 +05:30
|
|
|
|
} else {
|
|
|
|
|
|
_loadEvent();
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_autoScrollTimer?.cancel();
|
|
|
|
|
|
_pageController.dispose();
|
2026-03-18 17:00:25 +05:30
|
|
|
|
_pageNotifier.dispose();
|
2026-03-11 20:13:13 +05:30
|
|
|
|
_mapController?.dispose();
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Data loading
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-03-30 22:22:30 +05:30
|
|
|
|
/// Fetch full event details to fill in fields missing from the list
|
|
|
|
|
|
/// endpoint (important_information, images, etc.).
|
2026-03-30 20:20:15 +05:30
|
|
|
|
Future<void> _loadFullDetails() async {
|
2026-03-30 22:22:30 +05:30
|
|
|
|
for (int attempt = 0; attempt < 2; attempt++) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final ev = await _service.getEventDetails(widget.eventId);
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_event = ev;
|
|
|
|
|
|
});
|
|
|
|
|
|
_startAutoScroll();
|
|
|
|
|
|
return; // success
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
debugPrint('_loadFullDetails attempt ${attempt + 1} failed for event ${widget.eventId}: $e');
|
|
|
|
|
|
if (attempt == 0) {
|
|
|
|
|
|
await Future.delayed(const Duration(seconds: 1)); // wait before retry
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-30 20:20:15 +05:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
Future<void> _loadEvent() async {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_loading = true;
|
|
|
|
|
|
_error = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
try {
|
|
|
|
|
|
final ev = await _service.getEventDetails(widget.eventId);
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
setState(() => _event = ev);
|
2026-03-11 20:13:13 +05:30
|
|
|
|
_startAutoScroll();
|
2026-01-31 15:23:18 +05:30
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (!mounted) return;
|
security: sanitize all error messages shown to users
Created centralized userFriendlyError() utility that converts raw
exceptions into clean, user-friendly messages. Strips hostnames,
ports, OS error codes, HTTP status codes, stack traces, and Django
field names. Maps network/timeout/auth/server errors to plain
English messages.
Fixed 16 locations across 10 files:
- home_screen, calendar_screen, learn_more_screen (SnackBar/Text)
- login_screen, desktop_login_screen (SnackBar)
- profile_screen, contribute_screen, search_screen (SnackBar)
- review_form, review_section (inline error text)
- gamification_provider (error field)
Also removed double-wrapped exceptions in ReviewService (rethrow
instead of throw Exception('Failed to...: $e')).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:15:02 +05:30
|
|
|
|
setState(() => _error = userFriendlyError(e));
|
2026-01-31 15:23:18 +05:30
|
|
|
|
} finally {
|
|
|
|
|
|
if (mounted) setState(() => _loading = false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Carousel helpers
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
List<String> get _imageUrls {
|
|
|
|
|
|
final list = <String>[];
|
|
|
|
|
|
if (_event == null) return list;
|
|
|
|
|
|
final thumb = _event!.thumbImg;
|
2026-01-31 15:23:18 +05:30
|
|
|
|
if (thumb != null && thumb.isNotEmpty) list.add(thumb);
|
2026-03-11 20:13:13 +05:30
|
|
|
|
for (final img in _event!.images) {
|
|
|
|
|
|
if (img.image.isNotEmpty && !list.contains(img.image)) list.add(img.image);
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
2026-03-11 20:13:13 +05:30
|
|
|
|
return list;
|
|
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
void _startAutoScroll() {
|
|
|
|
|
|
_autoScrollTimer?.cancel();
|
|
|
|
|
|
final count = _imageUrls.length;
|
|
|
|
|
|
if (count <= 1) return;
|
|
|
|
|
|
_autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
|
|
|
|
|
if (!_pageController.hasClients) return;
|
2026-03-18 17:00:25 +05:30
|
|
|
|
final next = (_pageNotifier.value + 1) % count;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
_pageController.animateToPage(next,
|
|
|
|
|
|
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Date formatting
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
String _formattedDateRange() {
|
|
|
|
|
|
if (_event == null) return '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
final s = DateTime.parse(_event!.startDate);
|
|
|
|
|
|
final e = DateTime.parse(_event!.endDate);
|
|
|
|
|
|
const months = [
|
|
|
|
|
|
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
|
|
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
|
|
|
|
|
];
|
|
|
|
|
|
if (s.year == e.year && s.month == e.month && s.day == e.day) {
|
|
|
|
|
|
return '${s.day} ${months[s.month - 1]}';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s.month == e.month && s.year == e.year) {
|
|
|
|
|
|
return '${s.day} - ${e.day} ${months[s.month - 1]}';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '${s.day} ${months[s.month - 1]} - ${e.day} ${months[e.month - 1]}';
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return '${_event!.startDate} – ${_event!.endDate}';
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
2026-03-11 20:13:13 +05:30
|
|
|
|
}
|
2026-01-31 15:23:18 +05:30
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Actions
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _shareEvent() async {
|
|
|
|
|
|
final title = _event?.title ?? _event?.name ?? 'Check out this event';
|
|
|
|
|
|
final url =
|
|
|
|
|
|
'https://uat.eventifyplus.com/events/${widget.eventId}';
|
|
|
|
|
|
await Share.share('$title\n$url', subject: title);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _openUrl(String url) async {
|
|
|
|
|
|
final uri = Uri.parse(url);
|
|
|
|
|
|
if (await canLaunchUrl(uri)) {
|
|
|
|
|
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _viewLargerMap() {
|
|
|
|
|
|
if (_event?.latitude == null || _event?.longitude == null) return;
|
|
|
|
|
|
_openUrl(
|
|
|
|
|
|
'https://www.google.com/maps/search/?api=1&query=${_event!.latitude},${_event!.longitude}');
|
2026-01-31 15:23:18 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
void _getDirections() {
|
|
|
|
|
|
if (_event?.latitude == null || _event?.longitude == null) return;
|
|
|
|
|
|
_openUrl(
|
|
|
|
|
|
'https://www.google.com/maps/dir/?api=1&destination=${_event!.latitude},${_event!.longitude}');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// BUILD
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
if (_loading) {
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
body: _buildLoadingShimmer(theme),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_error != null) {
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
body: Center(
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(32),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(Icons.error_outline, size: 56, color: theme.colorScheme.error),
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
Text('Something went wrong',
|
|
|
|
|
|
style: theme.textTheme.titleMedium
|
|
|
|
|
|
?.copyWith(fontWeight: FontWeight.bold)),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Text(_error!, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
ElevatedButton.icon(
|
|
|
|
|
|
onPressed: _loadEvent,
|
|
|
|
|
|
icon: const Icon(Icons.refresh),
|
|
|
|
|
|
label: const Text('Retry'),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_event == null) {
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
body: const Center(child: Text('Event not found')),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 16:28:32 +05:30
|
|
|
|
final mediaQuery = MediaQuery.of(context);
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
final screenWidth = mediaQuery.size.width;
|
2026-03-18 16:28:32 +05:30
|
|
|
|
final screenHeight = mediaQuery.size.height;
|
2026-03-14 08:57:25 +05:30
|
|
|
|
final imageHeight = screenHeight * 0.45;
|
2026-03-18 16:28:32 +05:30
|
|
|
|
final topPadding = mediaQuery.padding.top;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
// ── DESKTOP layout ──────────────────────────────────────────────────
|
|
|
|
|
|
if (screenWidth >= AppConstants.desktopBreakpoint) {
|
|
|
|
|
|
final images = _imageUrls;
|
|
|
|
|
|
final heroImage = images.isNotEmpty ? images[0] : null;
|
|
|
|
|
|
final venueLabel = _event!.locationName ?? _event!.venueName ?? _event!.place ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
body: SingleChildScrollView(
|
|
|
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// ── Hero image with gradient overlay ──
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 300,
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Background image
|
|
|
|
|
|
if (heroImage != null)
|
|
|
|
|
|
CachedNetworkImage(
|
|
|
|
|
|
imageUrl: heroImage,
|
|
|
|
|
|
fit: BoxFit.cover,
|
2026-03-30 10:05:23 +05:30
|
|
|
|
memCacheWidth: 800,
|
|
|
|
|
|
memCacheHeight: 500,
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
|
|
|
|
|
Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Gradient overlay
|
|
|
|
|
|
Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topCenter,
|
|
|
|
|
|
end: Alignment.bottomCenter,
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
Colors.black.withOpacity(0.3),
|
|
|
|
|
|
Colors.black.withOpacity(0.65),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Top bar: back + share + wishlist
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
top: topPadding + 10,
|
|
|
|
|
|
left: 16,
|
|
|
|
|
|
right: 16,
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_squareIconButton(icon: Icons.arrow_back, onTap: () => Navigator.pop(context)),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
_squareIconButton(icon: Icons.ios_share_outlined, onTap: _shareEvent),
|
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
|
_squareIconButton(
|
|
|
|
|
|
icon: _wishlisted ? Icons.favorite : Icons.favorite_border,
|
|
|
|
|
|
iconColor: _wishlisted ? Colors.redAccent : Colors.white,
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
if (!AuthGuard.requireLogin(context, reason: 'Sign in to save events to your wishlist.')) return;
|
|
|
|
|
|
setState(() => _wishlisted = !_wishlisted);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Title + date + venue overlaid at bottom-left
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 32,
|
|
|
|
|
|
bottom: 28,
|
|
|
|
|
|
right: 200,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
_event!.title ?? _event!.name,
|
|
|
|
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontSize: 28,
|
|
|
|
|
|
height: 1.2,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const Icon(Icons.calendar_today_outlined, size: 16, color: Colors.white70),
|
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
_formattedDateRange(),
|
|
|
|
|
|
style: const TextStyle(color: Colors.white70, fontSize: 15),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (venueLabel.isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
|
const Icon(Icons.location_on_outlined, size: 16, color: Colors.white70),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Flexible(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
venueLabel,
|
|
|
|
|
|
style: const TextStyle(color: Colors.white70, fontSize: 15),
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
// "Book Your Spot" CTA on the right
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
right: 32,
|
|
|
|
|
|
bottom: 36,
|
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
// TODO: implement booking action
|
|
|
|
|
|
},
|
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
|
backgroundColor: const Color(0xFF1A56DB),
|
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 16),
|
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
|
),
|
|
|
|
|
|
elevation: 4,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Text(
|
|
|
|
|
|
'Book Your Spot',
|
|
|
|
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 28),
|
|
|
|
|
|
|
|
|
|
|
|
// ── Two-column: About (left 60%) + Venue/Map (right 40%) ──
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Left column — About the Event
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
flex: 3,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_buildAboutSection(theme),
|
|
|
|
|
|
if (_event!.importantInfo.isNotEmpty)
|
|
|
|
|
|
_buildImportantInfoSection(theme),
|
|
|
|
|
|
if (_event!.importantInfo.isEmpty &&
|
|
|
|
|
|
(_event!.importantInformation ?? '').isNotEmpty)
|
|
|
|
|
|
_buildImportantInfoFallback(theme),
|
feat: add complete review/rating system for events
New feature: Users can view, submit, and interact with event reviews.
Components added:
- ReviewModel, ReviewStatsModel, ReviewListResponse (models)
- ReviewService with getReviews, submitReview, markHelpful, flagReview
- StarRatingInput (interactive 5-star picker with labels)
- StarDisplay (read-only fractional star display)
- ReviewSummary (average rating + distribution bars)
- ReviewForm (star picker + comment field + submit/update)
- ReviewCard (avatar, timestamp, expandable comment, helpful/flag)
- ReviewSection (main container with pagination and state mgmt)
Integration:
- Added to LearnMoreScreen (both mobile and desktop layouts)
- Review API endpoints point to app.eventifyplus.com Node.js backend
- EventModel updated with averageRating/reviewCount fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:04:37 +05:30
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
ReviewSection(eventId: widget.eventId),
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 32),
|
|
|
|
|
|
// Right column — Venue / map
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
flex: 2,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
if (_event!.latitude != null && _event!.longitude != null) ...[
|
|
|
|
|
|
_buildVenueSection(theme),
|
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
_buildGetDirectionsButton(theme),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// ── Gallery: horizontal scrollable image strip ──
|
|
|
|
|
|
if (images.length > 1) ...[
|
|
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(left: 32),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
'Gallery',
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: 160,
|
|
|
|
|
|
child: ListView.builder(
|
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
|
|
|
|
itemCount: images.length > 6 ? 6 : images.length,
|
|
|
|
|
|
itemBuilder: (context, i) {
|
|
|
|
|
|
// Show overflow count badge on last visible item
|
|
|
|
|
|
final isLast = i == 5 && images.length > 6;
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(right: 12),
|
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(14),
|
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
CachedNetworkImage(
|
|
|
|
|
|
imageUrl: images[i],
|
|
|
|
|
|
fit: BoxFit.cover,
|
2026-03-30 10:05:23 +05:30
|
|
|
|
memCacheWidth: 800,
|
|
|
|
|
|
memCacheHeight: 500,
|
feat: rebuild desktop UI to match Figma + website, hero slider improvements
- Desktop sidebar (262px, blue gradient, white pill nav), topbar (search + bell + avatar), responsive shell rewritten
- Desktop homepage: immersive hero with Ken Burns animation, pill category chips, date badge cards matching mvnew.eventifyplus.com/home
- Desktop calendar: 60/40 two-column layout with white background
- Desktop profile: full-width banner + 3-column event grids
- Desktop learn more: hero image + about/venue columns + gallery strip
- Desktop settings/contribute: polished to match design system
- Mobile hero slider: RepaintBoundary, animated dots with 44px tap targets, 5s auto-scroll, 8s post-swipe delay, shimmer loading, dynamic event type badge, human-readable dates
- Guest access: requiresAuth false on read endpoints
- Location fix: show place names instead of lat/lng coordinates
- Version 1.6.1+17
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:28:19 +05:30
|
|
|
|
placeholder: (_, __) => Container(color: theme.dividerColor),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
child: Icon(Icons.broken_image, color: theme.hintColor),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (isLast)
|
|
|
|
|
|
Container(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.55),
|
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
'+${images.length - 6}',
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
fontSize: 24,
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 80),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── MOBILE layout ─────────────────────────────────────────────────────
|
2026-01-31 15:23:18 +05:30
|
|
|
|
return Scaffold(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
backgroundColor: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
body: Stack(
|
|
|
|
|
|
children: [
|
2026-03-14 08:57:25 +05:30
|
|
|
|
// ── Scrollable content (carousel + card scroll together) ──
|
2026-03-11 20:13:13 +05:30
|
|
|
|
SingleChildScrollView(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
2026-03-14 08:57:25 +05:30
|
|
|
|
// Image carousel (scrolls with content)
|
|
|
|
|
|
_buildImageCarousel(theme, imageHeight),
|
|
|
|
|
|
|
|
|
|
|
|
// Content card with rounded top corners overlapping carousel
|
|
|
|
|
|
Transform.translate(
|
|
|
|
|
|
offset: const Offset(0, -28),
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.scaffoldBackgroundColor,
|
|
|
|
|
|
borderRadius: const BorderRadius.vertical(
|
|
|
|
|
|
top: Radius.circular(28),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.08),
|
|
|
|
|
|
blurRadius: 20,
|
|
|
|
|
|
offset: const Offset(0, -6),
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
],
|
2026-03-14 08:57:25 +05:30
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_buildTitleSection(theme),
|
|
|
|
|
|
_buildAboutSection(theme),
|
|
|
|
|
|
if (_event!.latitude != null && _event!.longitude != null) ...[
|
|
|
|
|
|
_buildVenueSection(theme),
|
|
|
|
|
|
_buildGetDirectionsButton(theme),
|
|
|
|
|
|
],
|
|
|
|
|
|
if (_event!.importantInfo.isNotEmpty)
|
|
|
|
|
|
_buildImportantInfoSection(theme),
|
|
|
|
|
|
if (_event!.importantInfo.isEmpty &&
|
|
|
|
|
|
(_event!.importantInformation ?? '').isNotEmpty)
|
|
|
|
|
|
_buildImportantInfoFallback(theme),
|
feat: add complete review/rating system for events
New feature: Users can view, submit, and interact with event reviews.
Components added:
- ReviewModel, ReviewStatsModel, ReviewListResponse (models)
- ReviewService with getReviews, submitReview, markHelpful, flagReview
- StarRatingInput (interactive 5-star picker with labels)
- StarDisplay (read-only fractional star display)
- ReviewSummary (average rating + distribution bars)
- ReviewForm (star picker + comment field + submit/update)
- ReviewCard (avatar, timestamp, expandable comment, helpful/flag)
- ReviewSection (main container with pagination and state mgmt)
Integration:
- Added to LearnMoreScreen (both mobile and desktop layouts)
- Review API endpoints point to app.eventifyplus.com Node.js backend
- EventModel updated with averageRating/reviewCount fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:04:37 +05:30
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 0),
|
|
|
|
|
|
child: ReviewSection(eventId: widget.eventId),
|
|
|
|
|
|
),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
const SizedBox(height: 100),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2026-03-14 08:57:25 +05:30
|
|
|
|
// ── Fixed top bar with back/share/heart buttons ──
|
2026-03-11 20:13:13 +05:30
|
|
|
|
Positioned(
|
2026-03-14 08:57:25 +05:30
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: EdgeInsets.only(
|
|
|
|
|
|
top: topPadding + 10,
|
|
|
|
|
|
bottom: 10,
|
|
|
|
|
|
left: 16,
|
|
|
|
|
|
right: 16,
|
|
|
|
|
|
),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topCenter,
|
|
|
|
|
|
end: Alignment.bottomCenter,
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
Colors.black.withOpacity(0.5),
|
|
|
|
|
|
Colors.black.withOpacity(0.0),
|
|
|
|
|
|
],
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_squareIconButton(
|
|
|
|
|
|
icon: Icons.arrow_back,
|
|
|
|
|
|
onTap: () => Navigator.pop(context),
|
|
|
|
|
|
),
|
|
|
|
|
|
// Pill-shaped page indicators (centered)
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: _imageUrls.length > 1
|
2026-03-18 17:00:25 +05:30
|
|
|
|
? ValueListenableBuilder<int>(
|
|
|
|
|
|
valueListenable: _pageNotifier,
|
|
|
|
|
|
builder: (context, currentPage, _) => Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: List.generate(_imageUrls.length, (i) {
|
|
|
|
|
|
final active = i == currentPage;
|
|
|
|
|
|
return AnimatedContainer(
|
|
|
|
|
|
duration: const Duration(milliseconds: 300),
|
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 3),
|
|
|
|
|
|
width: active ? 18 : 8,
|
|
|
|
|
|
height: 6,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: active
|
|
|
|
|
|
? Colors.white
|
|
|
|
|
|
: Colors.white.withOpacity(0.45),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(3),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}),
|
|
|
|
|
|
),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
)
|
|
|
|
|
|
: const SizedBox.shrink(),
|
|
|
|
|
|
),
|
|
|
|
|
|
_squareIconButton(
|
|
|
|
|
|
icon: Icons.ios_share_outlined,
|
|
|
|
|
|
onTap: _shareEvent,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 10),
|
|
|
|
|
|
_squareIconButton(
|
|
|
|
|
|
icon: _wishlisted ? Icons.favorite : Icons.favorite_border,
|
|
|
|
|
|
iconColor: _wishlisted ? Colors.redAccent : Colors.white,
|
2026-03-21 07:29:55 +05:30
|
|
|
|
onTap: () {
|
|
|
|
|
|
if (!AuthGuard.requireLogin(context, reason: 'Sign in to save events to your wishlist.')) return;
|
|
|
|
|
|
setState(() => _wishlisted = !_wishlisted);
|
|
|
|
|
|
},
|
2026-03-14 08:57:25 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 1. LOADING SHIMMER
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildLoadingShimmer(ThemeData theme) {
|
2026-03-18 16:28:32 +05:30
|
|
|
|
final shimmerHeight = MediaQuery.of(context).size.height;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
return SafeArea(
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Placeholder image
|
|
|
|
|
|
Container(
|
2026-03-18 16:28:32 +05:30
|
|
|
|
height: shimmerHeight * 0.42,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor.withOpacity(0.3),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(28),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
|
// Placeholder title
|
|
|
|
|
|
Container(
|
|
|
|
|
|
height: 28,
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor.withOpacity(0.3),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
Container(
|
|
|
|
|
|
height: 16,
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor.withOpacity(0.3),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
Container(
|
|
|
|
|
|
height: 16,
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor.withOpacity(0.3),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Container(
|
|
|
|
|
|
height: 16,
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.dividerColor.withOpacity(0.3),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 2. IMAGE CAROUSEL WITH BLURRED BACKGROUND
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildImageCarousel(ThemeData theme, double carouselHeight) {
|
|
|
|
|
|
final images = _imageUrls;
|
|
|
|
|
|
final topPad = MediaQuery.of(context).padding.top;
|
|
|
|
|
|
|
|
|
|
|
|
return SizedBox(
|
|
|
|
|
|
height: carouselHeight,
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// ---- Blurred background (image or blue gradient) ----
|
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
|
child: images.isNotEmpty
|
|
|
|
|
|
? ClipRect(
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
|
children: [
|
2026-03-18 17:00:25 +05:30
|
|
|
|
ValueListenableBuilder<int>(
|
|
|
|
|
|
valueListenable: _pageNotifier,
|
|
|
|
|
|
builder: (context, currentPage, _) => CachedNetworkImage(
|
|
|
|
|
|
imageUrl: images[currentPage],
|
|
|
|
|
|
fit: BoxFit.cover,
|
2026-03-30 10:05:23 +05:30
|
|
|
|
memCacheWidth: 800,
|
|
|
|
|
|
memCacheHeight: 500,
|
2026-03-18 17:00:25 +05:30
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-18 17:00:25 +05:30
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
BackdropFilter(
|
|
|
|
|
|
filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25),
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.15),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Foreground image with rounded corners ----
|
|
|
|
|
|
if (images.isNotEmpty)
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
top: topPad + 56, // below the icon row
|
|
|
|
|
|
left: 20,
|
|
|
|
|
|
right: 20,
|
|
|
|
|
|
bottom: 16,
|
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
child: PageView.builder(
|
|
|
|
|
|
controller: _pageController,
|
2026-03-18 17:00:25 +05:30
|
|
|
|
onPageChanged: (i) => _pageNotifier.value = i,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
itemCount: images.length,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
itemBuilder: (_, i) => CachedNetworkImage(
|
|
|
|
|
|
imageUrl: images[i],
|
2026-03-11 20:13:13 +05:30
|
|
|
|
fit: BoxFit.cover,
|
2026-03-30 10:05:23 +05:30
|
|
|
|
memCacheWidth: 800,
|
|
|
|
|
|
memCacheHeight: 500,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
width: double.infinity,
|
release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p)
- Update pubspec.yaml version to 1.4.0+14
- Add CHANGELOG.md with full version history
- Update README.md: version badge + changelog section
- Desktop Contribute Dashboard rebuilt to match web version
- Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements)
- Two-column submit form, tier milestone progress bar
- Desktop leaderboard with podium, filters, rank table (green points)
- Desktop achievements 3-column badge grid
- Inline Reward Shop with RP balance
- Gamification feature module (EP, RP, leaderboard, achievements, shop)
- Profile screen redesigned to match web app layout with animations
- Home screen bottom sheet date filter chips
- Updated API endpoints, login/event detail screens, theme colors
- Added Gilroy font suite, responsive layout improvements
2026-03-18 11:10:56 +05:30
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
color: theme.dividerColor,
|
|
|
|
|
|
child: Icon(Icons.broken_image, size: 48, color: theme.hintColor),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// ---- No-image placeholder ----
|
|
|
|
|
|
if (images.isEmpty)
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
top: topPad + 56,
|
|
|
|
|
|
left: 20,
|
|
|
|
|
|
right: 20,
|
|
|
|
|
|
bottom: 16,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white.withOpacity(0.15),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Center(
|
|
|
|
|
|
child: Icon(Icons.event, size: 80, color: Colors.white70),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 08:57:25 +05:30
|
|
|
|
/// Square icon button with rounded corners and prominent background
|
2026-03-11 20:13:13 +05:30
|
|
|
|
Widget _squareIconButton({
|
|
|
|
|
|
required IconData icon,
|
|
|
|
|
|
required VoidCallback onTap,
|
|
|
|
|
|
Color iconColor = Colors.white,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: onTap,
|
|
|
|
|
|
child: Container(
|
2026-03-14 08:57:25 +05:30
|
|
|
|
width: 44,
|
|
|
|
|
|
height: 44,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
decoration: BoxDecoration(
|
2026-03-14 08:57:25 +05:30
|
|
|
|
color: Colors.black.withOpacity(0.35),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
border: Border.all(color: Colors.white.withOpacity(0.4)),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
child: Icon(icon, color: iconColor, size: 22),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 3. TITLE & DATE
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildTitleSection(ThemeData theme) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
_event!.title ?? _event!.name,
|
|
|
|
|
|
style: theme.textTheme.headlineSmall?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontSize: 26,
|
|
|
|
|
|
height: 1.25,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(Icons.calendar_today_outlined,
|
|
|
|
|
|
size: 16, color: theme.hintColor),
|
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
_formattedDateRange(),
|
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 4. ABOUT THE EVENT
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildAboutSection(ThemeData theme) {
|
|
|
|
|
|
final desc = _event!.description ?? '';
|
|
|
|
|
|
if (desc.isEmpty) return const SizedBox.shrink();
|
|
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'About the Event',
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
AnimatedCrossFade(
|
|
|
|
|
|
firstChild: Text(
|
|
|
|
|
|
desc,
|
|
|
|
|
|
maxLines: 4,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
|
height: 1.55,
|
|
|
|
|
|
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.75),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
secondChild: Text(
|
|
|
|
|
|
desc,
|
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
|
height: 1.55,
|
|
|
|
|
|
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.75),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
crossFadeState:
|
|
|
|
|
|
_aboutExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
|
|
|
|
|
duration: const Duration(milliseconds: 300),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: () => setState(() => _aboutExpanded = !_aboutExpanded),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
_aboutExpanded ? 'Read Less' : 'Read More',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-03-30 20:09:50 +05:30
|
|
|
|
// 5. VENUE LOCATION (Native Google Map on mobile, fallback on web)
|
2026-03-11 20:13:13 +05:30
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildVenueSection(ThemeData theme) {
|
|
|
|
|
|
final lat = _event!.latitude!;
|
|
|
|
|
|
final lng = _event!.longitude!;
|
|
|
|
|
|
final venueLabel = _event!.locationName ?? _event!.venueName ?? _event!.place ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'Venue Location',
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
|
|
|
|
|
|
|
// Map container
|
|
|
|
|
|
ClipRRect(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
child: SizedBox(
|
2026-03-30 20:09:50 +05:30
|
|
|
|
height: 250,
|
|
|
|
|
|
width: double.infinity,
|
2026-03-11 20:13:13 +05:30
|
|
|
|
child: Stack(
|
|
|
|
|
|
children: [
|
2026-03-30 20:09:50 +05:30
|
|
|
|
// Native Google Maps SDK on mobile, tappable fallback on web
|
2026-03-14 08:57:25 +05:30
|
|
|
|
if (kIsWeb)
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: _viewLargerMap,
|
|
|
|
|
|
child: Container(
|
2026-03-30 20:09:50 +05:30
|
|
|
|
color: const Color(0xFFE8EAF6),
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(Icons.map_outlined, size: 48, color: theme.colorScheme.primary),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Text('Tap to view on Google Maps',
|
|
|
|
|
|
style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
)
|
|
|
|
|
|
else
|
|
|
|
|
|
GoogleMap(
|
|
|
|
|
|
initialCameraPosition: CameraPosition(
|
|
|
|
|
|
target: LatLng(lat, lng),
|
|
|
|
|
|
zoom: 15,
|
|
|
|
|
|
),
|
|
|
|
|
|
markers: {
|
|
|
|
|
|
Marker(
|
|
|
|
|
|
markerId: const MarkerId('event'),
|
|
|
|
|
|
position: LatLng(lat, lng),
|
|
|
|
|
|
infoWindow: InfoWindow(title: venueLabel),
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
myLocationButtonEnabled: false,
|
2026-03-30 20:09:50 +05:30
|
|
|
|
zoomControlsEnabled: true,
|
2026-03-14 08:57:25 +05:30
|
|
|
|
scrollGesturesEnabled: true,
|
|
|
|
|
|
rotateGesturesEnabled: false,
|
|
|
|
|
|
tiltGesturesEnabled: false,
|
|
|
|
|
|
onMapCreated: (c) => _mapController = c,
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
2026-03-30 20:09:50 +05:30
|
|
|
|
// "View larger map" overlay button — top left
|
2026-03-11 20:13:13 +05:30
|
|
|
|
Positioned(
|
|
|
|
|
|
top: 10,
|
|
|
|
|
|
left: 10,
|
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
|
onTap: _viewLargerMap,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
|
boxShadow: [
|
2026-03-30 20:09:50 +05:30
|
|
|
|
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 6),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-30 20:09:50 +05:30
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
children: [
|
2026-03-30 20:09:50 +05:30
|
|
|
|
Icon(Icons.open_in_new, size: 14, color: theme.colorScheme.primary),
|
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'View larger map',
|
|
|
|
|
|
style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600, fontSize: 13),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-03-30 20:09:50 +05:30
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Venue name card
|
|
|
|
|
|
if (venueLabel.isNotEmpty)
|
|
|
|
|
|
Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
margin: const EdgeInsets.only(top: 14),
|
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.cardColor,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
2026-03-30 20:09:50 +05:30
|
|
|
|
color: theme.shadowColor.withValues(alpha: 0.06),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
blurRadius: 12,
|
|
|
|
|
|
offset: const Offset(0, 4),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
2026-03-30 20:09:50 +05:30
|
|
|
|
Text(venueLabel, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
if (_event!.place != null && _event!.place != venueLabel)
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(top: 4),
|
2026-03-30 20:09:50 +05:30
|
|
|
|
child: Text(_event!.place!, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 6. GET DIRECTIONS BUTTON
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildGetDirectionsButton(ThemeData theme) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 18, 20, 0),
|
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 54,
|
|
|
|
|
|
child: ElevatedButton.icon(
|
|
|
|
|
|
onPressed: _getDirections,
|
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
|
backgroundColor: theme.colorScheme.primary,
|
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
),
|
|
|
|
|
|
elevation: 2,
|
|
|
|
|
|
),
|
|
|
|
|
|
icon: const Icon(Icons.directions, size: 22),
|
|
|
|
|
|
label: const Text(
|
|
|
|
|
|
'Get Directions',
|
|
|
|
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 7. IMPORTANT INFORMATION (structured list)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildImportantInfoSection(ThemeData theme) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'Important Information',
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
|
for (final info in _event!.importantInfo)
|
|
|
|
|
|
Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.05),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
border: Border.all(
|
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.12),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Container(
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.1),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Icon(Icons.info_outline,
|
|
|
|
|
|
size: 20, color: theme.colorScheme.primary),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
info['title'] ?? '',
|
|
|
|
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
info['value'] ?? '',
|
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
|
height: 1.4,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 7b. IMPORTANT INFO FALLBACK (parse HTML string into cards)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// Strip HTML tags and decode common HTML entities
|
|
|
|
|
|
String _stripHtml(String html) {
|
|
|
|
|
|
// Remove all HTML tags
|
|
|
|
|
|
var text = html.replaceAll(RegExp(r'<[^>]*>'), '');
|
|
|
|
|
|
// Decode common HTML entities
|
|
|
|
|
|
text = text
|
|
|
|
|
|
.replaceAll('&', '&')
|
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
|
.replaceAll('>', '>')
|
|
|
|
|
|
.replaceAll('"', '"')
|
|
|
|
|
|
.replaceAll(''', "'")
|
|
|
|
|
|
.replaceAll(' ', ' ');
|
|
|
|
|
|
return text.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Parse an HTML important_information string into a list of {title, value} maps
|
|
|
|
|
|
List<Map<String, String>> _parseHtmlImportantInfo(String raw) {
|
2026-03-30 22:13:38 +05:30
|
|
|
|
var text = raw;
|
|
|
|
|
|
// 1. Remove <style>...</style> blocks entirely (content + tags)
|
|
|
|
|
|
text = text.replaceAll(RegExp(r'<style[^>]*>.*?</style>', caseSensitive: false, dotAll: true), '');
|
|
|
|
|
|
// 2. Remove <script>...</script> blocks
|
|
|
|
|
|
text = text.replaceAll(RegExp(r'<script[^>]*>.*?</script>', caseSensitive: false, dotAll: true), '');
|
|
|
|
|
|
// 3. Convert block-level closers to newlines
|
|
|
|
|
|
text = text.replaceAll(RegExp(r'</div>', caseSensitive: false), '\n');
|
|
|
|
|
|
text = text.replaceAll(RegExp(r'</p>', caseSensitive: false), '\n');
|
|
|
|
|
|
text = text.replaceAll(RegExp(r'</li>', caseSensitive: false), '\n');
|
|
|
|
|
|
// 4. Convert <br> to newlines
|
|
|
|
|
|
text = text.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n');
|
|
|
|
|
|
// 5. Strip all remaining HTML tags
|
|
|
|
|
|
text = text.replaceAll(RegExp(r'<[^>]*>'), '');
|
|
|
|
|
|
// 6. Decode HTML entities
|
2026-03-11 20:13:13 +05:30
|
|
|
|
text = text
|
|
|
|
|
|
.replaceAll('&', '&')
|
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
|
.replaceAll('>', '>')
|
|
|
|
|
|
.replaceAll('"', '"')
|
|
|
|
|
|
.replaceAll(''', "'")
|
|
|
|
|
|
.replaceAll(' ', ' ');
|
|
|
|
|
|
|
|
|
|
|
|
// Split by newlines first
|
|
|
|
|
|
var lines = text
|
|
|
|
|
|
.split('\n')
|
|
|
|
|
|
.map((l) => l.trim())
|
|
|
|
|
|
.where((l) => l.isNotEmpty)
|
|
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
|
|
|
|
// If we only have 1 line, items might be separated by emoji characters
|
|
|
|
|
|
// (some categories don't use <br> between items, e.g. "...etc.🚌 Bus:")
|
|
|
|
|
|
if (lines.length <= 1 && text.trim().isNotEmpty) {
|
|
|
|
|
|
final parts = text.trim().split(
|
|
|
|
|
|
RegExp(r'(?=[\u2600-\u27BF]|[\u{1F300}-\u{1FFFF}])', unicode: true),
|
|
|
|
|
|
);
|
|
|
|
|
|
final emojiLines = parts
|
|
|
|
|
|
.map((l) => l.trim())
|
|
|
|
|
|
.where((l) => l.isNotEmpty)
|
|
|
|
|
|
.toList();
|
|
|
|
|
|
if (emojiLines.length > 1) {
|
|
|
|
|
|
lines = emojiLines;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final items = <Map<String, String>>[];
|
|
|
|
|
|
for (final line in lines) {
|
|
|
|
|
|
// Split on first colon to get title:value
|
|
|
|
|
|
final colonIdx = line.indexOf(':');
|
|
|
|
|
|
if (colonIdx > 0 && colonIdx < line.length - 1) {
|
|
|
|
|
|
items.add({
|
|
|
|
|
|
'title': line.substring(0, colonIdx + 1).trim(),
|
|
|
|
|
|
'value': line.substring(colonIdx + 1).trim(),
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
items.add({'title': line, 'value': ''});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return items;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildImportantInfoFallback(ThemeData theme) {
|
|
|
|
|
|
final parsed = _parseHtmlImportantInfo(_event!.importantInformation!);
|
|
|
|
|
|
|
|
|
|
|
|
if (parsed.isEmpty) return const SizedBox.shrink();
|
|
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'Important Information',
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
|
for (final info in parsed)
|
|
|
|
|
|
Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.05),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
border: Border.all(
|
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.12),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Container(
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.1),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Icon(Icons.info_outline,
|
|
|
|
|
|
size: 20, color: theme.colorScheme.primary),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
info['title'] ?? '',
|
|
|
|
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if ((info['value'] ?? '').isNotEmpty) ...[
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
info['value']!,
|
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
|
height: 1.4,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|