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';
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
class LearnMoreScreen extends StatefulWidget {
|
|
|
|
|
|
final int eventId;
|
|
|
|
|
|
const LearnMoreScreen({Key? key, required this.eventId}) : super(key: key);
|
|
|
|
|
|
|
|
|
|
|
|
@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();
|
|
|
|
|
|
int _currentPage = 0;
|
|
|
|
|
|
Timer? _autoScrollTimer;
|
|
|
|
|
|
|
|
|
|
|
|
// About section
|
|
|
|
|
|
bool _aboutExpanded = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Wishlist (UI-only)
|
|
|
|
|
|
bool _wishlisted = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Google Map
|
|
|
|
|
|
GoogleMapController? _mapController;
|
|
|
|
|
|
MapType _mapType = MapType.normal;
|
|
|
|
|
|
bool _showMapControls = false;
|
|
|
|
|
|
|
2026-01-31 15:23:18 +05:30
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
_loadEvent();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 20:13:13 +05:30
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_autoScrollTimer?.cancel();
|
|
|
|
|
|
_pageController.dispose();
|
|
|
|
|
|
_mapController?.dispose();
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Data loading
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
setState(() => _error = e.toString());
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
final next = (_currentPage + 1) % count;
|
|
|
|
|
|
_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}');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Map camera helpers
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void _moveCamera(double latDelta, double lngDelta) {
|
|
|
|
|
|
_mapController?.animateCamera(CameraUpdate.scrollBy(lngDelta * 80, -latDelta * 80));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _zoom(double amount) {
|
|
|
|
|
|
_mapController?.animateCamera(CameraUpdate.zoomBy(amount));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 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')),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final screenHeight = MediaQuery.of(context).size.height;
|
2026-03-14 08:57:25 +05:30
|
|
|
|
final imageHeight = screenHeight * 0.45;
|
|
|
|
|
|
final topPadding = MediaQuery.of(context).padding.top;
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
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),
|
|
|
|
|
|
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
|
|
|
|
|
|
? 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),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
: 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,
|
|
|
|
|
|
onTap: () => setState(() => _wishlisted = !_wishlisted),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 1. LOADING SHIMMER
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildLoadingShimmer(ThemeData theme) {
|
|
|
|
|
|
return SafeArea(
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// Placeholder image
|
|
|
|
|
|
Container(
|
|
|
|
|
|
height: MediaQuery.of(context).size.height * 0.42,
|
|
|
|
|
|
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: [
|
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
|
|
|
|
CachedNetworkImage(
|
|
|
|
|
|
imageUrl: images[_currentPage],
|
2026-03-11 20:13:13 +05:30
|
|
|
|
fit: BoxFit.cover,
|
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
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
|
placeholder: (_, __) => Container(
|
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
|
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
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,
|
|
|
|
|
|
onPageChanged: (i) => setState(() => _currentPage = i),
|
|
|
|
|
|
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,
|
|
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 5. VENUE LOCATION (Google Map)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
|
height: 280,
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
children: [
|
2026-03-14 08:57:25 +05:30
|
|
|
|
// Use static map image on web (Google Maps JS SDK not configured),
|
|
|
|
|
|
// native GoogleMap widget on mobile
|
|
|
|
|
|
if (kIsWeb)
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: _viewLargerMap,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.grey.shade200,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Positioned.fill(
|
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
|
|
|
|
child: CachedNetworkImage(
|
|
|
|
|
|
imageUrl: 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x300&markers=color:red%7C$lat,$lng&key=',
|
2026-03-14 08:57:25 +05:30
|
|
|
|
fit: BoxFit.cover,
|
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
|
|
|
|
errorWidget: (_, __, ___) => Container(
|
2026-03-14 08:57:25 +05:30
|
|
|
|
color: const Color(0xFFE8EAF6),
|
|
|
|
|
|
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-11 20:13:13 +05:30
|
|
|
|
),
|
2026-03-14 08:57:25 +05:30
|
|
|
|
)
|
|
|
|
|
|
else
|
|
|
|
|
|
GoogleMap(
|
|
|
|
|
|
initialCameraPosition: CameraPosition(
|
|
|
|
|
|
target: LatLng(lat, lng),
|
|
|
|
|
|
zoom: 15,
|
|
|
|
|
|
),
|
|
|
|
|
|
mapType: _mapType,
|
|
|
|
|
|
markers: {
|
|
|
|
|
|
Marker(
|
|
|
|
|
|
markerId: const MarkerId('event'),
|
|
|
|
|
|
position: LatLng(lat, lng),
|
|
|
|
|
|
infoWindow: InfoWindow(title: venueLabel),
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
myLocationButtonEnabled: false,
|
|
|
|
|
|
zoomControlsEnabled: false,
|
|
|
|
|
|
scrollGesturesEnabled: true,
|
|
|
|
|
|
rotateGesturesEnabled: false,
|
|
|
|
|
|
tiltGesturesEnabled: false,
|
|
|
|
|
|
onMapCreated: (c) => _mapController = c,
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
|
|
|
|
|
|
// "View larger map" – top left
|
|
|
|
|
|
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: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.12),
|
|
|
|
|
|
blurRadius: 6,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
'View larger map',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: theme.colorScheme.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2026-03-14 08:57:25 +05:30
|
|
|
|
// Map type toggle – bottom left (native only)
|
|
|
|
|
|
if (!kIsWeb)
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
bottom: 12,
|
|
|
|
|
|
left: 12,
|
|
|
|
|
|
child: _mapControlButton(
|
|
|
|
|
|
icon: _mapType == MapType.normal
|
|
|
|
|
|
? Icons.satellite_alt
|
|
|
|
|
|
: Icons.map_outlined,
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_mapType = _mapType == MapType.normal
|
|
|
|
|
|
? MapType.satellite
|
|
|
|
|
|
: MapType.normal;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
|
2026-03-14 08:57:25 +05:30
|
|
|
|
// Map controls toggle – bottom right (native only)
|
|
|
|
|
|
if (!kIsWeb)
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
bottom: 12,
|
|
|
|
|
|
right: 12,
|
|
|
|
|
|
child: _mapControlButton(
|
|
|
|
|
|
icon: Icons.open_with_rounded,
|
|
|
|
|
|
onTap: () => setState(() => _showMapControls = !_showMapControls),
|
|
|
|
|
|
),
|
2026-03-11 20:13:13 +05:30
|
|
|
|
),
|
|
|
|
|
|
|
2026-03-14 08:57:25 +05:30
|
|
|
|
// Directional pad overlay (native only)
|
|
|
|
|
|
if (!kIsWeb && _showMapControls)
|
2026-03-11 20:13:13 +05:30
|
|
|
|
Positioned.fill(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.25),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
|
),
|
2026-01-31 15:23:18 +05:30
|
|
|
|
child: Column(
|
2026-03-11 20:13:13 +05:30
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2026-01-31 15:23:18 +05:30
|
|
|
|
children: [
|
2026-03-11 20:13:13 +05:30
|
|
|
|
Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_mapControlButton(
|
|
|
|
|
|
icon: Icons.keyboard_arrow_up,
|
|
|
|
|
|
onTap: () => _moveCamera(1, 0)),
|
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
|
_mapControlButton(
|
|
|
|
|
|
icon: Icons.add,
|
|
|
|
|
|
onTap: () => _zoom(1)),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_mapControlButton(
|
|
|
|
|
|
icon: Icons.keyboard_arrow_left,
|
|
|
|
|
|
onTap: () => _moveCamera(0, -1)),
|
|
|
|
|
|
const SizedBox(width: 60),
|
|
|
|
|
|
_mapControlButton(
|
|
|
|
|
|
icon: Icons.keyboard_arrow_right,
|
|
|
|
|
|
onTap: () => _moveCamera(0, 1)),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_mapControlButton(
|
|
|
|
|
|
icon: Icons.keyboard_arrow_down,
|
|
|
|
|
|
onTap: () => _moveCamera(-1, 0)),
|
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
|
_mapControlButton(
|
|
|
|
|
|
icon: Icons.remove,
|
|
|
|
|
|
onTap: () => _zoom(-1)),
|
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
|
_mapControlButton(
|
|
|
|
|
|
icon: Icons.close,
|
|
|
|
|
|
onTap: () =>
|
|
|
|
|
|
setState(() => _showMapControls = false)),
|
|
|
|
|
|
],
|
2026-01-31 15:23:18 +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(
|
|
|
|
|
|
color: theme.shadowColor.withOpacity(0.06),
|
|
|
|
|
|
blurRadius: 12,
|
|
|
|
|
|
offset: const Offset(0, 4),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
venueLabel,
|
|
|
|
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (_event!.place != null && _event!.place != venueLabel)
|
|
|
|
|
|
Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(top: 4),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
_event!.place!,
|
|
|
|
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
|
|
|
|
color: theme.hintColor,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _mapControlButton({
|
|
|
|
|
|
required IconData icon,
|
|
|
|
|
|
required VoidCallback onTap,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: onTap,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 44,
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white.withOpacity(0.92),
|
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: Colors.black.withOpacity(0.15),
|
|
|
|
|
|
blurRadius: 6,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Icon(icon, color: Colors.grey.shade700, size: 22),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 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) {
|
|
|
|
|
|
// Strip HTML tags, preserving <br> as a newline separator first
|
|
|
|
|
|
var text = raw
|
|
|
|
|
|
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
|
|
|
|
|
|
.replaceAll(RegExp(r'<[^>]*>'), '');
|
|
|
|
|
|
// Decode entities
|
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|