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
This commit is contained in:
2026-03-18 11:10:56 +05:30
parent 5b98f41596
commit 50caad21a5
16 changed files with 3219 additions and 921 deletions

View File

@@ -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())));
}