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 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,10 @@ import 'profile_screen.dart';
|
||||
import 'booking_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'learn_more_screen.dart';
|
||||
import 'contribute_screen.dart';
|
||||
import '../core/app_decoration.dart';
|
||||
import '../features/gamification/providers/gamification_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HomeDesktopScreen extends StatefulWidget {
|
||||
final bool skipSidebarEntranceAnimation;
|
||||
@@ -978,35 +981,9 @@ class _HomeDesktopScreenState extends State<HomeDesktopScreen> with SingleTicker
|
||||
case 3:
|
||||
return BookingScreen(onBook: () {}, image: '');
|
||||
case 4:
|
||||
// Contribute placeholder (kept simple)
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('Contribute', style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 14),
|
||||
const Text('Submit events or contact the Eventify team.'),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(children: [
|
||||
TextField(decoration: InputDecoration(labelText: 'Event title', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
|
||||
const SizedBox(height: 12),
|
||||
TextField(decoration: InputDecoration(labelText: 'Location', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
|
||||
const SizedBox(height: 12),
|
||||
TextField(maxLines: 4, decoration: InputDecoration(labelText: 'Description', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)))),
|
||||
const SizedBox(height: 12),
|
||||
Row(children: [
|
||||
ElevatedButton(onPressed: () {}, child: const Text('Submit')),
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton(onPressed: () {}, child: const Text('Reset')),
|
||||
])
|
||||
]),
|
||||
),
|
||||
),
|
||||
]),
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => GamificationProvider(),
|
||||
child: const ContributeScreen(),
|
||||
);
|
||||
case 5:
|
||||
return const SettingsScreen();
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
import '../features/events/models/event_models.dart';
|
||||
import '../features/events/services/events_service.dart';
|
||||
@@ -13,6 +14,8 @@ import 'learn_more_screen.dart';
|
||||
import 'search_screen.dart';
|
||||
import '../core/app_decoration.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../features/gamification/providers/gamification_provider.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({Key? key}) : super(key: key);
|
||||
@@ -78,13 +81,19 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
_pincode = prefs.getString('pincode') ?? 'all';
|
||||
|
||||
try {
|
||||
final types = await _events_service_getEventTypesSafe();
|
||||
final events = await _events_service_getEventsSafe(_pincode);
|
||||
// Fetch types and events in parallel for faster loading
|
||||
final results = await Future.wait([
|
||||
_events_service_getEventTypesSafe(),
|
||||
_events_service_getEventsSafe(_pincode),
|
||||
]);
|
||||
final types = results[0] as List<EventTypeModel>;
|
||||
final events = results[1] as List<EventModel>;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_types = types;
|
||||
_events = events;
|
||||
_selectedTypeId = -1;
|
||||
_cachedFilteredEvents = null; // invalidate cache
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -157,10 +166,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
child: imageUrl != null && imageUrl.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => Icon(
|
||||
placeholder: (_, __) => Icon(
|
||||
icon ?? Icons.category,
|
||||
size: 36,
|
||||
color: selected ? Colors.white.withOpacity(0.5) : theme.colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
errorWidget: (_, __, ___) => Icon(
|
||||
icon ?? Icons.category,
|
||||
size: 36,
|
||||
color: selected ? Colors.white : theme.colorScheme.primary,
|
||||
@@ -362,7 +376,10 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
children: [
|
||||
_buildHomeContent(), // index 0
|
||||
const CalendarScreen(), // index 1
|
||||
const ContributeScreen(), // index 2 (full page, scrollable)
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => GamificationProvider(),
|
||||
child: const ContributeScreen(),
|
||||
), // index 2 (full page, scrollable)
|
||||
const ProfileScreen(), // index 3
|
||||
],
|
||||
),
|
||||
@@ -445,11 +462,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
String _selectedDateFilter = '';
|
||||
DateTime? _selectedCustomDate;
|
||||
|
||||
// Cached filtered events to avoid repeated DateTime.parse() calls
|
||||
List<EventModel>? _cachedFilteredEvents;
|
||||
String _cachedFilterKey = '';
|
||||
|
||||
/// Returns the subset of [_events] that match the active date-filter chip.
|
||||
/// If no chip is selected the full list is returned.
|
||||
/// Uses caching to avoid re-parsing dates on every access.
|
||||
List<EventModel> get _filteredEvents {
|
||||
if (_selectedDateFilter.isEmpty) return _events;
|
||||
|
||||
// Build a cache key from filter state
|
||||
final cacheKey = '$_selectedDateFilter|$_selectedCustomDate|${_events.length}';
|
||||
if (_cachedFilteredEvents != null && _cachedFilterKey == cacheKey) {
|
||||
return _cachedFilteredEvents!;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
|
||||
@@ -481,7 +508,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
return _events;
|
||||
}
|
||||
|
||||
return _events.where((e) {
|
||||
_cachedFilteredEvents = _events.where((e) {
|
||||
try {
|
||||
final eStart = DateTime.parse(e.startDate);
|
||||
final eEnd = DateTime.parse(e.endDate);
|
||||
@@ -491,6 +518,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
_cachedFilterKey = cacheKey;
|
||||
return _cachedFilteredEvents!;
|
||||
}
|
||||
|
||||
Future<void> _onDateChipTap(String label) async {
|
||||
@@ -501,6 +530,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
setState(() {
|
||||
_selectedCustomDate = picked;
|
||||
_selectedDateFilter = 'Date';
|
||||
_cachedFilteredEvents = null; // invalidate cache
|
||||
});
|
||||
_showFilteredEventsSheet(
|
||||
'${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}',
|
||||
@@ -509,12 +539,14 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
setState(() {
|
||||
_selectedDateFilter = '';
|
||||
_selectedCustomDate = null;
|
||||
_cachedFilteredEvents = null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_selectedDateFilter = label;
|
||||
_selectedCustomDate = null;
|
||||
_cachedFilteredEvents = null; // invalidate cache
|
||||
});
|
||||
_showFilteredEventsSheet(label);
|
||||
}
|
||||
@@ -663,12 +695,17 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('http')) {
|
||||
imageWidget = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
placeholder: (_, __) => Container(
|
||||
width: 80, height: 80,
|
||||
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
|
||||
child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))),
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
width: 80, height: 80,
|
||||
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(Icons.image, color: Colors.grey.shade400),
|
||||
@@ -1426,10 +1463,16 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
children: [
|
||||
// Background image
|
||||
img != null && img.isNotEmpty
|
||||
? Image.network(
|
||||
img,
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
placeholder: (_, __) => Container(
|
||||
color: const Color(0xFF374151),
|
||||
child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white38))),
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: const Color(0xFF374151),
|
||||
child: const Icon(Icons.image, color: Colors.white38, size: 40),
|
||||
),
|
||||
@@ -1613,7 +1656,14 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: img != null && img.isNotEmpty
|
||||
? Image.network(img, width: 96, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 96, color: theme.dividerColor))
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
width: 96,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => Container(width: 96, color: theme.dividerColor, child: const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))),
|
||||
errorWidget: (_, __, ___) => Container(width: 96, color: theme.dividerColor),
|
||||
)
|
||||
: Container(width: 96, height: double.infinity, color: theme.dividerColor),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
@@ -1671,12 +1721,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: img != null && img.isNotEmpty
|
||||
? Image.network(
|
||||
img,
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
width: 220,
|
||||
height: 180,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
placeholder: (_, __) => Container(
|
||||
width: 220,
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))),
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
width: 220,
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
@@ -1833,12 +1892,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
child: img != null && img.isNotEmpty
|
||||
? Image.network(
|
||||
img,
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
placeholder: (_, __) => Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: const Center(child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2))),
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
@@ -1961,7 +2029,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
try {
|
||||
final all = await _eventsService.getEventsByPincode(_pincode);
|
||||
final filtered = id == -1 ? all : all.where((e) => e.eventTypeId == id).toList();
|
||||
if (mounted) setState(() => _events = filtered);
|
||||
if (mounted) setState(() { _events = filtered; _cachedFilteredEvents = null; });
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
import '../features/events/models/event_models.dart';
|
||||
import '../features/events/services/events_service.dart';
|
||||
@@ -430,10 +431,21 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.network(
|
||||
images[_currentPage],
|
||||
CachedNetworkImage(
|
||||
imageUrl: images[_currentPage],
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
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(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
@@ -476,11 +488,15 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
controller: _pageController,
|
||||
onPageChanged: (i) => setState(() => _currentPage = i),
|
||||
itemCount: images.length,
|
||||
itemBuilder: (_, i) => Image.network(
|
||||
images[i],
|
||||
itemBuilder: (_, i) => CachedNetworkImage(
|
||||
imageUrl: images[i],
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
placeholder: (_, __) => Container(
|
||||
color: theme.dividerColor,
|
||||
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: theme.dividerColor,
|
||||
child: Icon(Icons.broken_image, size: 48, color: theme.hintColor),
|
||||
),
|
||||
@@ -672,10 +688,10 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.network(
|
||||
'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x300&markers=color:red%7C$lat,$lng&key=',
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x300&markers=color:red%7C$lat,$lng&key=',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: const Color(0xFFE8EAF6),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
Reference in New Issue
Block a user