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

4
.gitignore vendored
View File

@@ -46,3 +46,7 @@ app.*.map.json
/android/app/profile
/android/app/release
web/assets/login-bg.mp4
# Keystore files (signing keys)
*.jks
*.keystore

71
CHANGELOG.md Normal file
View File

@@ -0,0 +1,71 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
## [1.4.0] - 2026-03-18
### Added
- **Desktop Contribute Dashboard**: Full desktop layout for the Contribute screen, matching the web version at eventifyplus.com/contribute
- "Contributor Dashboard" title with 3-tab navigation (Contribute, Leaderboard, Achievements)
- Two-column submit event form — Event Title + Category side-by-side, Date + Location side-by-side
- Contributor Level gradient card with 5-tier milestone progress bar (Bronze → Silver → Gold → Platinum → Diamond)
- Sub-navigation row: My Events / Submit Event / Reward Shop
- Desktop Leaderboard with All Time / This Month toggle, district pills, podium, and full rank table
- Desktop Achievements with 3-column badge grid, progress bars, and lock icons
- Inline Reward Shop with RP balance badge and shop item cards
- **Gamification Feature Module** (`lib/features/gamification/`):
- `GamificationProvider` — ChangeNotifier-based state management
- `GamificationService` — mock data for EP, RP, leaderboard entries, achievements, and shop items
- Models: `LeaderboardUser`, `Achievement`, `ShopItem`, `ContributorStats`
- **Bottom Sheet Date Filters**: Home screen event-category filter chips now open in a modal bottom sheet on mobile
- Web runner script (`run_web.sh`) for local Flutter web development server
### Changed
- **Profile Screen**: Completely redesigned to match the web app layout — gradient header card, avatar, stats row (Likes / Posts / Views), and tabbed content
- **Profile Card Animations**: Smooth entrance animations matching the React web component
- **Contribute Screen (Mobile)**: Full 4-tab rebuild — Contribute, Leaderboard, Achievements, Shop — with animated glass-glider tab bar indicator
- **Login Screen**: Updated UI design aligned with the web version
- **Event Detail Screen**: Layout updates and improved API data binding
- **Theme**: Refreshed dark/light mode colour palette and surface colours
- **API Client**: Updated base URL and endpoint paths in `lib/core/api/api_endpoints.dart`
- **Fonts**: Integrated full Gilroy font family (Light, Regular, Medium, SemiBold, Bold, ExtraBold — with italic variants)
- **Responsive Layout**: Improved breakpoint handling; desktop threshold set at 820 px
### Fixed
- Profile card pixel-perfect alignment with the web version
- Calendar screen date-range filter and location search integration
- District dropdown naming conflict in leaderboard (`_lbDistricts` vs. `_districts`)
- Green points colour (#16A34A) on desktop leaderboard matching web (was blue #0F45CF on mobile)
---
## [1.3.0] - 2026-02-xx
### Added
- Leaderboard tab and Achievements tab added to the Contribute screen
- Bouncy sliding glass-glider animation for Contribute tab bar
---
## [1.2.0] - 2026-01-xx
### Added
- Responsive dual-layout system with 820 px breakpoint
- Date filtering on the Home screen event feed
- Location search integration
- Calendar screen bug fixes and improvements
---
## [1.1.0] - 2025-12-xx
### Added
- Initial screens: Login, Home, Events, Profile, Calendar, Search, Booking
- Desktop variants: `DesktopLoginScreen`, `HomeDesktopScreen`
- Flutter launcher icons and native splash screen
- Gilroy font integration (initial)
- `shared_preferences` session caching

View File

@@ -17,6 +17,7 @@
[![Flutter](https://img.shields.io/badge/Flutter-%2302569B.svg?style=for-the-badge&logo=flutter&logoColor=white)](https://flutter.dev/)
[![Dart](https://img.shields.io/badge/Dart-%230175C2.svg?style=for-the-badge&logo=dart&logoColor=white)](https://dart.dev/)
[![Platform](https://img.shields.io/badge/Platform-iOS%20%7C%20Android%20%7C%20Web%20%7C%20Desktop-lightgrey?style=for-the-badge)](#)
[![Version](https://img.shields.io/badge/version-1.4.0--preview-blue?style=for-the-badge)](#)
</div>
@@ -123,6 +124,18 @@ The app uses an initialization check in `main.dart` that intercepts the launch v
---
## 📋 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for a full history of changes.
### Latest (v1.4.0 — Preview)
- **Desktop Contribute Dashboard** rebuilt to match the web version (Contributor Dashboard, 3-tab nav, two-column form, leaderboard, achievements, reward shop)
- **Gamification module** — EP, RP, leaderboard, achievements, shop with Provider state management
- **Profile screen** redesigned to match the web app layout with animations
- Enhanced animations, responsive improvements, Gilroy font suite, and API updates
---
<div align="center">
<sub>Built with ❤️ by the Eventify Team</sub>
</div>

View File

@@ -22,8 +22,8 @@ android {
applicationId = "com.sicherhaven.eventify"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = 11
versionName = "1.2(p)"
versionCode = 14
versionName = "1.4(p)"
}
// ---------- SIGNING CONFIG ----------

View File

@@ -23,4 +23,12 @@ class ApiEndpoints {
// static const String bookEvent = "$baseUrl/events/book-event/";
// static const String userSuccessBookings = "$baseUrl/events/event/user-success-bookings/";
// static const String userCancelledBookings = "$baseUrl/events/event/user-cancelled-bookings/";
// Gamification / Contributor Module (TechDocs v2)
static const String gamificationProfile = "$baseUrl/v1/user/gamification-profile/";
static const String leaderboard = "$baseUrl/v1/leaderboard/";
static const String shopItems = "$baseUrl/v1/shop/items/";
static const String shopRedeem = "$baseUrl/v1/shop/redeem/";
static const String contributeSubmit = "$baseUrl/v1/contributions/submit/";
static const String gradeContribution = "$baseUrl/v1/admin/contributions/"; // append {id}/grade/
}

View File

@@ -0,0 +1,212 @@
// lib/features/gamification/models/gamification_models.dart
// Data models matching TechDocs v2 DB schema for the Contributor Module.
// ---------------------------------------------------------------------------
// Tier enum — matches PRD v3 §4.4 thresholds (based on Lifetime EP)
// ---------------------------------------------------------------------------
enum ContributorTier { BRONZE, SILVER, GOLD, PLATINUM, DIAMOND }
/// Returns the correct tier for a given lifetime EP total.
ContributorTier tierFromEp(int lifetimeEp) {
if (lifetimeEp >= 5000) return ContributorTier.DIAMOND;
if (lifetimeEp >= 1500) return ContributorTier.PLATINUM;
if (lifetimeEp >= 500) return ContributorTier.GOLD;
if (lifetimeEp >= 100) return ContributorTier.SILVER;
return ContributorTier.BRONZE;
}
/// Human-readable label for a tier.
String tierLabel(ContributorTier tier) {
switch (tier) {
case ContributorTier.BRONZE:
return 'Bronze';
case ContributorTier.SILVER:
return 'Silver';
case ContributorTier.GOLD:
return 'Gold';
case ContributorTier.PLATINUM:
return 'Platinum';
case ContributorTier.DIAMOND:
return 'Diamond';
}
}
/// EP threshold for next tier (used for progress bar). Returns null at max tier.
int? nextTierThreshold(ContributorTier tier) {
switch (tier) {
case ContributorTier.BRONZE:
return 100;
case ContributorTier.SILVER:
return 500;
case ContributorTier.GOLD:
return 1500;
case ContributorTier.PLATINUM:
return 5000;
case ContributorTier.DIAMOND:
return null;
}
}
/// Lower EP bound for current tier (used for progress bar calculation).
int tierStartEp(ContributorTier tier) {
switch (tier) {
case ContributorTier.BRONZE:
return 0;
case ContributorTier.SILVER:
return 100;
case ContributorTier.GOLD:
return 500;
case ContributorTier.PLATINUM:
return 1500;
case ContributorTier.DIAMOND:
return 5000;
}
}
// ---------------------------------------------------------------------------
// UserGamificationProfile — mirrors the `UserGamificationProfile` DB table
// ---------------------------------------------------------------------------
class UserGamificationProfile {
final String userId;
final int lifetimeEp; // Never resets. Used for tiers + leaderboard.
final int currentEp; // Liquid EP accumulated this month.
final int currentRp; // Spendable Reward Points.
final ContributorTier tier;
const UserGamificationProfile({
required this.userId,
required this.lifetimeEp,
required this.currentEp,
required this.currentRp,
required this.tier,
});
factory UserGamificationProfile.fromJson(Map<String, dynamic> json) {
final ep = (json['lifetime_ep'] as int?) ?? 0;
return UserGamificationProfile(
userId: json['user_id'] as String? ?? '',
lifetimeEp: ep,
currentEp: (json['current_ep'] as int?) ?? 0,
currentRp: (json['current_rp'] as int?) ?? 0,
tier: tierFromEp(ep),
);
}
}
// ---------------------------------------------------------------------------
// LeaderboardEntry
// ---------------------------------------------------------------------------
class LeaderboardEntry {
final int rank;
final String username;
final String? avatarUrl;
final int lifetimeEp;
final ContributorTier tier;
final int eventsCount;
final bool isCurrentUser;
const LeaderboardEntry({
required this.rank,
required this.username,
this.avatarUrl,
required this.lifetimeEp,
required this.tier,
required this.eventsCount,
this.isCurrentUser = false,
});
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) {
final ep = (json['lifetime_ep'] as int?) ?? 0;
return LeaderboardEntry(
rank: (json['rank'] as int?) ?? 0,
username: json['username'] as String? ?? '',
avatarUrl: json['avatar_url'] as String?,
lifetimeEp: ep,
tier: tierFromEp(ep),
eventsCount: (json['events_count'] as int?) ?? 0,
isCurrentUser: (json['is_current_user'] as bool?) ?? false,
);
}
}
// ---------------------------------------------------------------------------
// ShopItem — mirrors `RedeemShopItem` table
// ---------------------------------------------------------------------------
class ShopItem {
final String id;
final String name;
final String description;
final int rpCost;
final int stockQuantity;
final String? imageUrl;
const ShopItem({
required this.id,
required this.name,
required this.description,
required this.rpCost,
required this.stockQuantity,
this.imageUrl,
});
factory ShopItem.fromJson(Map<String, dynamic> json) {
return ShopItem(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
description: json['description'] as String? ?? '',
rpCost: (json['rp_cost'] as int?) ?? 0,
stockQuantity: (json['stock_quantity'] as int?) ?? 0,
imageUrl: json['image_url'] as String?,
);
}
}
// ---------------------------------------------------------------------------
// RedemptionRecord — mirrors `RedemptionHistory` table
// ---------------------------------------------------------------------------
class RedemptionRecord {
final String id;
final String itemId;
final int rpSpent;
final String voucherCode;
final DateTime timestamp;
const RedemptionRecord({
required this.id,
required this.itemId,
required this.rpSpent,
required this.voucherCode,
required this.timestamp,
});
factory RedemptionRecord.fromJson(Map<String, dynamic> json) {
return RedemptionRecord(
id: json['id'] as String? ?? '',
itemId: json['item_id'] as String? ?? '',
rpSpent: (json['rp_spent'] as int?) ?? 0,
voucherCode: json['voucher_code_issued'] as String? ?? '',
timestamp: DateTime.tryParse(json['timestamp'] as String? ?? '') ?? DateTime.now(),
);
}
}
// ---------------------------------------------------------------------------
// AchievementBadge
// ---------------------------------------------------------------------------
class AchievementBadge {
final String id;
final String title;
final String description;
final String iconName; // maps to an IconData key
final bool isUnlocked;
final double progress; // 0.0 1.0
const AchievementBadge({
required this.id,
required this.title,
required this.description,
required this.iconName,
required this.isUnlocked,
required this.progress,
});
}

View File

@@ -0,0 +1,124 @@
// lib/features/gamification/providers/gamification_provider.dart
import 'package:flutter/foundation.dart';
import '../models/gamification_models.dart';
import '../services/gamification_service.dart';
class GamificationProvider extends ChangeNotifier {
final GamificationService _service = GamificationService();
// State
UserGamificationProfile? profile;
List<LeaderboardEntry> leaderboard = [];
List<ShopItem> shopItems = [];
List<AchievementBadge> achievements = [];
// Leaderboard filters — matches web version
String leaderboardDistrict = 'Overall Kerala';
String leaderboardTimePeriod = 'all_time'; // 'all_time' | 'this_month'
bool isLoading = false;
String? error;
// ---------------------------------------------------------------------------
// Load everything at once (called when ContributeScreen is mounted)
// ---------------------------------------------------------------------------
Future<void> loadAll() async {
isLoading = true;
error = null;
notifyListeners();
try {
final results = await Future.wait([
_service.getProfile(),
_service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod),
_service.getShopItems(),
_service.getAchievements(),
]);
profile = results[0] as UserGamificationProfile;
leaderboard = results[1] as List<LeaderboardEntry>;
shopItems = results[2] as List<ShopItem>;
achievements = results[3] as List<AchievementBadge>;
} catch (e) {
error = e.toString();
} finally {
isLoading = false;
notifyListeners();
}
}
// ---------------------------------------------------------------------------
// Change district filter
// ---------------------------------------------------------------------------
Future<void> setDistrict(String district) async {
if (leaderboardDistrict == district) return;
leaderboardDistrict = district;
notifyListeners();
try {
leaderboard = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
} catch (e) {
error = e.toString();
}
notifyListeners();
}
// ---------------------------------------------------------------------------
// Change time period filter
// ---------------------------------------------------------------------------
Future<void> setTimePeriod(String period) async {
if (leaderboardTimePeriod == period) return;
leaderboardTimePeriod = period;
notifyListeners();
try {
leaderboard = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
} catch (e) {
error = e.toString();
}
notifyListeners();
}
// ---------------------------------------------------------------------------
// Redeem a shop item — deducts RP locally optimistically, returns voucher code
// ---------------------------------------------------------------------------
Future<String> redeemItem(String itemId) async {
final item = shopItems.firstWhere((s) => s.id == itemId);
// Optimistically deduct RP
if (profile != null) {
profile = UserGamificationProfile(
userId: profile!.userId,
lifetimeEp: profile!.lifetimeEp,
currentEp: profile!.currentEp,
currentRp: profile!.currentRp - item.rpCost,
tier: profile!.tier,
);
notifyListeners();
}
try {
final record = await _service.redeemItem(itemId);
return record.voucherCode;
} catch (e) {
// Rollback on failure
if (profile != null) {
profile = UserGamificationProfile(
userId: profile!.userId,
lifetimeEp: profile!.lifetimeEp,
currentEp: profile!.currentEp,
currentRp: profile!.currentRp + item.rpCost,
tier: profile!.tier,
);
notifyListeners();
}
rethrow;
}
}
// ---------------------------------------------------------------------------
// Submit a contribution
// ---------------------------------------------------------------------------
Future<void> submitContribution(Map<String, dynamic> data) async {
await _service.submitContribution(data);
}
}

View File

@@ -0,0 +1,180 @@
// lib/features/gamification/services/gamification_service.dart
//
// Stub service using the real API contract from TechDocs v2.
// All methods currently return mock data.
// TODO: replace each mock block with a real ApiClient call once
// the backend endpoints are live on uat.eventifyplus.com.
import 'dart:math';
import '../models/gamification_models.dart';
class GamificationService {
// ---------------------------------------------------------------------------
// User Gamification Profile
// TODO: replace with ApiClient.get(ApiEndpoints.gamificationProfile)
// ---------------------------------------------------------------------------
Future<UserGamificationProfile> getProfile() async {
await Future.delayed(const Duration(milliseconds: 400));
return const UserGamificationProfile(
userId: 'mock-user-001',
lifetimeEp: 320,
currentEp: 70,
currentRp: 45,
tier: ContributorTier.SILVER,
);
}
// ---------------------------------------------------------------------------
// Leaderboard
// district: 'Overall Kerala' | 'Thiruvananthapuram' | 'Kollam' | ...
// timePeriod: 'all_time' | 'this_month'
// TODO: replace with ApiClient.get(ApiEndpoints.leaderboard, params: {'district': district, 'period': timePeriod})
// ---------------------------------------------------------------------------
Future<List<LeaderboardEntry>> getLeaderboard({
required String district,
required String timePeriod,
}) async {
await Future.delayed(const Duration(milliseconds: 500));
// Realistic mock names per district
final names = [
'Annette Black', 'Jerome Bell', 'Theresa Webb', 'Courtney Henry',
'Cameron Williamson', 'Dianne Russell', 'Wade Warren', 'Albert Flores',
'Kristin Watson', 'Guy Hawkins',
];
final rng = Random(district.hashCode ^ timePeriod.hashCode);
final baseEp = timePeriod == 'this_month' ? 800 : 4500;
final entries = List.generate(10, (i) {
final ep = baseEp - (i * (timePeriod == 'this_month' ? 55 : 280)) + rng.nextInt(30);
return LeaderboardEntry(
rank: i + 1,
username: names[i],
lifetimeEp: ep,
tier: tierFromEp(ep),
eventsCount: 149 - i * 12,
isCurrentUser: i == 7, // mock: current user is rank 8
);
});
return entries;
}
// ---------------------------------------------------------------------------
// Redeem Shop Items
// TODO: replace with ApiClient.get(ApiEndpoints.shopItems)
// ---------------------------------------------------------------------------
Future<List<ShopItem>> getShopItems() async {
await Future.delayed(const Duration(milliseconds: 400));
return const [
ShopItem(
id: 'item-001',
name: 'Amazon ₹500 Voucher',
description: 'Redeem for any purchase on Amazon India.',
rpCost: 50,
stockQuantity: 20,
),
ShopItem(
id: 'item-002',
name: 'Swiggy ₹200 Voucher',
description: 'Free food delivery credit on Swiggy.',
rpCost: 20,
stockQuantity: 35,
),
ShopItem(
id: 'item-003',
name: 'Eventify Pro — 1 Month',
description: 'Premium access to Eventify.Plus features.',
rpCost: 30,
stockQuantity: 100,
),
ShopItem(
id: 'item-004',
name: 'Zomato ₹150 Voucher',
description: 'Discount on your next Zomato order.',
rpCost: 15,
stockQuantity: 50,
),
ShopItem(
id: 'item-005',
name: 'BookMyShow ₹300 Voucher',
description: 'Movie & event ticket credit on BookMyShow.',
rpCost: 30,
stockQuantity: 15,
),
ShopItem(
id: 'item-006',
name: 'Exclusive Badge',
description: 'Rare "Pioneer" badge for your profile.',
rpCost: 5,
stockQuantity: 0, // out of stock
),
];
}
// ---------------------------------------------------------------------------
// Redeem an item
// TODO: replace with ApiClient.post(ApiEndpoints.shopRedeem, body: {'item_id': itemId})
// ---------------------------------------------------------------------------
Future<RedemptionRecord> redeemItem(String itemId) async {
await Future.delayed(const Duration(milliseconds: 600));
// Generate a fake voucher code
final code = 'EVT-${itemId.toUpperCase().replaceAll('-', '').substring(0, 4)}-${Random().nextInt(9000) + 1000}';
return RedemptionRecord(
id: 'redemption-${DateTime.now().millisecondsSinceEpoch}',
itemId: itemId,
rpSpent: 0, // provider will look up cost
voucherCode: code,
timestamp: DateTime.now(),
);
}
// ---------------------------------------------------------------------------
// Submit Contribution
// TODO: replace with ApiClient.post(ApiEndpoints.contributeSubmit, body: data)
// ---------------------------------------------------------------------------
Future<void> submitContribution(Map<String, dynamic> data) async {
await Future.delayed(const Duration(milliseconds: 800));
// Mock always succeeds
}
// ---------------------------------------------------------------------------
// Achievements
// ---------------------------------------------------------------------------
Future<List<AchievementBadge>> getAchievements() async {
await Future.delayed(const Duration(milliseconds: 300));
return const [
AchievementBadge(
id: 'badge-01', title: 'First Submission',
description: 'Submitted your first event.',
iconName: 'edit', isUnlocked: true, progress: 1.0,
),
AchievementBadge(
id: 'badge-02', title: 'Silver Streak',
description: 'Reached Silver tier.',
iconName: 'star', isUnlocked: true, progress: 1.0,
),
AchievementBadge(
id: 'badge-03', title: 'Gold Rush',
description: 'Reach Gold tier (500 EP).',
iconName: 'emoji_events', isUnlocked: false, progress: 0.64,
),
AchievementBadge(
id: 'badge-04', title: 'Top 10',
description: 'Appear in the district leaderboard top 10.',
iconName: 'leaderboard', isUnlocked: false, progress: 0.5,
),
AchievementBadge(
id: 'badge-05', title: 'Image Pro',
description: 'Submit 10 events with 3+ images.',
iconName: 'photo_library', isUnlocked: false, progress: 0.3,
),
AchievementBadge(
id: 'badge-06', title: 'Pioneer',
description: 'One of the first 100 contributors.',
iconName: 'verified', isUnlocked: true, progress: 1.0,
),
];
}
}

File diff suppressed because it is too large Load Diff

View File

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

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

View File

@@ -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,

View File

@@ -10,6 +10,7 @@ import geolocator_apple
import path_provider_foundation
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import video_player_avfoundation
@@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
}

View File

@@ -41,6 +41,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
characters:
dependency: transitive
description:
@@ -182,6 +206,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -520,6 +552,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
@@ -616,6 +664,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.3"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
sanitize_html:
dependency: transitive
description:
@@ -717,6 +781,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev"
source: hosted
version: "2.4.2+3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
@@ -749,6 +853,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
table_calendar:
dependency: "direct main"
description:

View File

@@ -1,7 +1,7 @@
name: figma
description: A Flutter event app
publish_to: 'none'
version: 1.0.0+1
version: 1.4.0+14
environment:
sdk: ">=2.17.0 <3.0.0"
@@ -19,7 +19,9 @@ dependencies:
google_maps_flutter: ^2.5.0
url_launcher: ^6.2.1
share_plus: ^7.2.1
provider: ^6.1.2
video_player: ^2.8.1
cached_network_image: ^3.3.1
dev_dependencies:
flutter_test:

3
run_web.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
cd /Users/bshtechnologies/Documents/Eventify-frontend
exec flutter run -d web-server --web-hostname 0.0.0.0 --web-port "${PORT:-8080}"