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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -46,3 +46,7 @@ app.*.map.json
|
|||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
web/assets/login-bg.mp4
|
web/assets/login-bg.mp4
|
||||||
|
|
||||||
|
# Keystore files (signing keys)
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|||||||
71
CHANGELOG.md
Normal file
71
CHANGELOG.md
Normal 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
|
||||||
13
README.md
13
README.md
@@ -17,6 +17,7 @@
|
|||||||
[](https://flutter.dev/)
|
[](https://flutter.dev/)
|
||||||
[](https://dart.dev/)
|
[](https://dart.dev/)
|
||||||
[](#)
|
[](#)
|
||||||
|
[](#)
|
||||||
|
|
||||||
</div>
|
</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">
|
<div align="center">
|
||||||
<sub>Built with ❤️ by the Eventify Team</sub>
|
<sub>Built with ❤️ by the Eventify Team</sub>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ android {
|
|||||||
applicationId = "com.sicherhaven.eventify"
|
applicationId = "com.sicherhaven.eventify"
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = 11
|
versionCode = 14
|
||||||
versionName = "1.2(p)"
|
versionName = "1.4(p)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- SIGNING CONFIG ----------
|
// ---------- SIGNING CONFIG ----------
|
||||||
|
|||||||
@@ -23,4 +23,12 @@ class ApiEndpoints {
|
|||||||
// static const String bookEvent = "$baseUrl/events/book-event/";
|
// static const String bookEvent = "$baseUrl/events/book-event/";
|
||||||
// static const String userSuccessBookings = "$baseUrl/events/event/user-success-bookings/";
|
// static const String userSuccessBookings = "$baseUrl/events/event/user-success-bookings/";
|
||||||
// static const String userCancelledBookings = "$baseUrl/events/event/user-cancelled-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/
|
||||||
}
|
}
|
||||||
|
|||||||
212
lib/features/gamification/models/gamification_models.dart
Normal file
212
lib/features/gamification/models/gamification_models.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
124
lib/features/gamification/providers/gamification_provider.dart
Normal file
124
lib/features/gamification/providers/gamification_provider.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
lib/features/gamification/services/gamification_service.dart
Normal file
180
lib/features/gamification/services/gamification_service.dart
Normal 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
@@ -10,7 +10,10 @@ import 'profile_screen.dart';
|
|||||||
import 'booking_screen.dart';
|
import 'booking_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
import 'learn_more_screen.dart';
|
import 'learn_more_screen.dart';
|
||||||
|
import 'contribute_screen.dart';
|
||||||
import '../core/app_decoration.dart';
|
import '../core/app_decoration.dart';
|
||||||
|
import '../features/gamification/providers/gamification_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class HomeDesktopScreen extends StatefulWidget {
|
class HomeDesktopScreen extends StatefulWidget {
|
||||||
final bool skipSidebarEntranceAnimation;
|
final bool skipSidebarEntranceAnimation;
|
||||||
@@ -978,35 +981,9 @@ class _HomeDesktopScreenState extends State<HomeDesktopScreen> with SingleTicker
|
|||||||
case 3:
|
case 3:
|
||||||
return BookingScreen(onBook: () {}, image: '');
|
return BookingScreen(onBook: () {}, image: '');
|
||||||
case 4:
|
case 4:
|
||||||
// Contribute placeholder (kept simple)
|
return ChangeNotifierProvider(
|
||||||
return SingleChildScrollView(
|
create: (_) => GamificationProvider(),
|
||||||
padding: const EdgeInsets.all(28),
|
child: const ContributeScreen(),
|
||||||
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')),
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
case 5:
|
case 5:
|
||||||
return const SettingsScreen();
|
return const SettingsScreen();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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/models/event_models.dart';
|
||||||
import '../features/events/services/events_service.dart';
|
import '../features/events/services/events_service.dart';
|
||||||
@@ -13,6 +14,8 @@ import 'learn_more_screen.dart';
|
|||||||
import 'search_screen.dart';
|
import 'search_screen.dart';
|
||||||
import '../core/app_decoration.dart';
|
import '../core/app_decoration.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../features/gamification/providers/gamification_provider.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({Key? key}) : super(key: key);
|
const HomeScreen({Key? key}) : super(key: key);
|
||||||
@@ -78,13 +81,19 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
_pincode = prefs.getString('pincode') ?? 'all';
|
_pincode = prefs.getString('pincode') ?? 'all';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final types = await _events_service_getEventTypesSafe();
|
// Fetch types and events in parallel for faster loading
|
||||||
final events = await _events_service_getEventsSafe(_pincode);
|
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) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_types = types;
|
_types = types;
|
||||||
_events = events;
|
_events = events;
|
||||||
_selectedTypeId = -1;
|
_selectedTypeId = -1;
|
||||||
|
_cachedFilteredEvents = null; // invalidate cache
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -157,10 +166,15 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
child: imageUrl != null && imageUrl.isNotEmpty
|
child: imageUrl != null && imageUrl.isNotEmpty
|
||||||
? ClipRRect(
|
? ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.network(
|
child: CachedNetworkImage(
|
||||||
imageUrl,
|
imageUrl: imageUrl,
|
||||||
fit: BoxFit.contain,
|
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,
|
icon ?? Icons.category,
|
||||||
size: 36,
|
size: 36,
|
||||||
color: selected ? Colors.white : theme.colorScheme.primary,
|
color: selected ? Colors.white : theme.colorScheme.primary,
|
||||||
@@ -362,7 +376,10 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
children: [
|
children: [
|
||||||
_buildHomeContent(), // index 0
|
_buildHomeContent(), // index 0
|
||||||
const CalendarScreen(), // index 1
|
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
|
const ProfileScreen(), // index 3
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -445,11 +462,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
String _selectedDateFilter = '';
|
String _selectedDateFilter = '';
|
||||||
DateTime? _selectedCustomDate;
|
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.
|
/// 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 {
|
List<EventModel> get _filteredEvents {
|
||||||
if (_selectedDateFilter.isEmpty) return _events;
|
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 now = DateTime.now();
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
@@ -481,7 +508,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
return _events;
|
return _events;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _events.where((e) {
|
_cachedFilteredEvents = _events.where((e) {
|
||||||
try {
|
try {
|
||||||
final eStart = DateTime.parse(e.startDate);
|
final eStart = DateTime.parse(e.startDate);
|
||||||
final eEnd = DateTime.parse(e.endDate);
|
final eEnd = DateTime.parse(e.endDate);
|
||||||
@@ -491,6 +518,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
_cachedFilterKey = cacheKey;
|
||||||
|
return _cachedFilteredEvents!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDateChipTap(String label) async {
|
Future<void> _onDateChipTap(String label) async {
|
||||||
@@ -501,6 +530,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
setState(() {
|
setState(() {
|
||||||
_selectedCustomDate = picked;
|
_selectedCustomDate = picked;
|
||||||
_selectedDateFilter = 'Date';
|
_selectedDateFilter = 'Date';
|
||||||
|
_cachedFilteredEvents = null; // invalidate cache
|
||||||
});
|
});
|
||||||
_showFilteredEventsSheet(
|
_showFilteredEventsSheet(
|
||||||
'${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}',
|
'${picked.day.toString().padLeft(2, '0')} ${_monthName(picked.month)} ${picked.year}',
|
||||||
@@ -509,12 +539,14 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
setState(() {
|
setState(() {
|
||||||
_selectedDateFilter = '';
|
_selectedDateFilter = '';
|
||||||
_selectedCustomDate = null;
|
_selectedCustomDate = null;
|
||||||
|
_cachedFilteredEvents = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDateFilter = label;
|
_selectedDateFilter = label;
|
||||||
_selectedCustomDate = null;
|
_selectedCustomDate = null;
|
||||||
|
_cachedFilteredEvents = null; // invalidate cache
|
||||||
});
|
});
|
||||||
_showFilteredEventsSheet(label);
|
_showFilteredEventsSheet(label);
|
||||||
}
|
}
|
||||||
@@ -663,12 +695,17 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('http')) {
|
if (imageUrl != null && imageUrl.isNotEmpty && imageUrl.startsWith('http')) {
|
||||||
imageWidget = ClipRRect(
|
imageWidget = ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.network(
|
child: CachedNetworkImage(
|
||||||
imageUrl,
|
imageUrl: imageUrl,
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
fit: BoxFit.cover,
|
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,
|
width: 80, height: 80,
|
||||||
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
|
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(12)),
|
||||||
child: Icon(Icons.image, color: Colors.grey.shade400),
|
child: Icon(Icons.image, color: Colors.grey.shade400),
|
||||||
@@ -1426,10 +1463,16 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
children: [
|
children: [
|
||||||
// Background image
|
// Background image
|
||||||
img != null && img.isNotEmpty
|
img != null && img.isNotEmpty
|
||||||
? Image.network(
|
? CachedNetworkImage(
|
||||||
img,
|
imageUrl: img,
|
||||||
fit: BoxFit.cover,
|
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),
|
color: const Color(0xFF374151),
|
||||||
child: const Icon(Icons.image, color: Colors.white38, size: 40),
|
child: const Icon(Icons.image, color: Colors.white38, size: 40),
|
||||||
),
|
),
|
||||||
@@ -1613,7 +1656,14 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: img != null && img.isNotEmpty
|
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),
|
: Container(width: 96, height: double.infinity, color: theme.dividerColor),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
@@ -1671,12 +1721,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(18),
|
||||||
child: img != null && img.isNotEmpty
|
child: img != null && img.isNotEmpty
|
||||||
? Image.network(
|
? CachedNetworkImage(
|
||||||
img,
|
imageUrl: img,
|
||||||
width: 220,
|
width: 220,
|
||||||
height: 180,
|
height: 180,
|
||||||
fit: BoxFit.cover,
|
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,
|
width: 220,
|
||||||
height: 180,
|
height: 180,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1833,12 +1892,21 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
child: img != null && img.isNotEmpty
|
child: img != null && img.isNotEmpty
|
||||||
? Image.network(
|
? CachedNetworkImage(
|
||||||
img,
|
imageUrl: img,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 200,
|
height: 200,
|
||||||
fit: BoxFit.cover,
|
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,
|
width: double.infinity,
|
||||||
height: 200,
|
height: 200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1961,7 +2029,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
try {
|
try {
|
||||||
final all = await _eventsService.getEventsByPincode(_pincode);
|
final all = await _eventsService.getEventsByPincode(_pincode);
|
||||||
final filtered = id == -1 ? all : all.where((e) => e.eventTypeId == id).toList();
|
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) {
|
} catch (e) {
|
||||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
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:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:url_launcher/url_launcher.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/models/event_models.dart';
|
||||||
import '../features/events/services/events_service.dart';
|
import '../features/events/services/events_service.dart';
|
||||||
@@ -430,10 +431,21 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
Image.network(
|
CachedNetworkImage(
|
||||||
images[_currentPage],
|
imageUrl: images[_currentPage],
|
||||||
fit: BoxFit.cover,
|
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(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
@@ -476,11 +488,15 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
onPageChanged: (i) => setState(() => _currentPage = i),
|
onPageChanged: (i) => setState(() => _currentPage = i),
|
||||||
itemCount: images.length,
|
itemCount: images.length,
|
||||||
itemBuilder: (_, i) => Image.network(
|
itemBuilder: (_, i) => CachedNetworkImage(
|
||||||
images[i],
|
imageUrl: images[i],
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
placeholder: (_, __) => Container(
|
||||||
|
color: theme.dividerColor,
|
||||||
|
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
|
),
|
||||||
|
errorWidget: (_, __, ___) => Container(
|
||||||
color: theme.dividerColor,
|
color: theme.dividerColor,
|
||||||
child: Icon(Icons.broken_image, size: 48, color: theme.hintColor),
|
child: Icon(Icons.broken_image, size: 48, color: theme.hintColor),
|
||||||
),
|
),
|
||||||
@@ -672,10 +688,10 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.network(
|
child: CachedNetworkImage(
|
||||||
'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x300&markers=color:red%7C$lat,$lng&key=',
|
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,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorWidget: (_, __, ___) => Container(
|
||||||
color: const Color(0xFFE8EAF6),
|
color: const Color(0xFFE8EAF6),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import geolocator_apple
|
|||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import video_player_avfoundation
|
import video_player_avfoundation
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
112
pubspec.lock
112
pubspec.lock
@@ -41,6 +41,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
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:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -182,6 +206,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -520,6 +552,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.6"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -616,6 +664,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
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:
|
sanitize_html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -717,6 +781,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
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:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -749,6 +853,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
table_calendar:
|
table_calendar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: figma
|
name: figma
|
||||||
description: A Flutter event app
|
description: A Flutter event app
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.0+1
|
version: 1.4.0+14
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
@@ -19,7 +19,9 @@ dependencies:
|
|||||||
google_maps_flutter: ^2.5.0
|
google_maps_flutter: ^2.5.0
|
||||||
url_launcher: ^6.2.1
|
url_launcher: ^6.2.1
|
||||||
share_plus: ^7.2.1
|
share_plus: ^7.2.1
|
||||||
|
provider: ^6.1.2
|
||||||
video_player: ^2.8.1
|
video_player: ^2.8.1
|
||||||
|
cached_network_image: ^3.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
3
run_web.sh
Executable file
3
run_web.sh
Executable 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}"
|
||||||
Reference in New Issue
Block a user