Initial commit: Eventify frontend
This commit is contained in:
36
lib/features/auth/models/user_model.dart
Normal file
36
lib/features/auth/models/user_model.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
class UserModel {
|
||||
final String username;
|
||||
final String email;
|
||||
final String role;
|
||||
final String token;
|
||||
final String? phoneNumber;
|
||||
|
||||
UserModel({
|
||||
required this.username,
|
||||
required this.email,
|
||||
required this.role,
|
||||
required this.token,
|
||||
this.phoneNumber,
|
||||
});
|
||||
|
||||
/// Defensive factory: uses defaults when keys are missing
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserModel(
|
||||
username: (json['username'] ?? json['email'] ?? '').toString(),
|
||||
email: (json['email'] ?? '').toString(),
|
||||
role: (json['role'] ?? 'user').toString(),
|
||||
token: (json['token'] ?? '').toString(),
|
||||
phoneNumber: json['phone_number'] != null ? json['phone_number'].toString() : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'role': role,
|
||||
'token': token,
|
||||
if (phoneNumber != null) 'phone_number': phoneNumber,
|
||||
};
|
||||
}
|
||||
}
|
||||
67
lib/features/auth/providers/auth_provider.dart
Normal file
67
lib/features/auth/providers/auth_provider.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/user_model.dart';
|
||||
import '../services/auth_service.dart';
|
||||
|
||||
class AuthProvider extends ChangeNotifier {
|
||||
final AuthService _authService = AuthService();
|
||||
|
||||
UserModel? _user;
|
||||
bool _loading = false;
|
||||
|
||||
UserModel? get user => _user;
|
||||
bool get loading => _loading;
|
||||
|
||||
/// Login with username/email and password.
|
||||
Future<void> login(String username, String password) async {
|
||||
_loading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final user = await _authService.login(username, password);
|
||||
_user = user;
|
||||
} finally {
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register wrapper that is compatible with both old and new callers.
|
||||
///
|
||||
/// Preferred: provide [email], [phoneNumber], [password].
|
||||
/// Older callers that pass [username] will still work — username is ignored
|
||||
/// for the backend call (we use email/phone/password as per API spec).
|
||||
Future<void> register({
|
||||
String? username, // kept for backward compatibility; not sent to backend
|
||||
String? email,
|
||||
required String password,
|
||||
String? phoneNumber,
|
||||
}) async {
|
||||
_loading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Prefer explicit email param; if not provided try to use username (some screens passed email as username)
|
||||
final resolvedEmail = (email != null && email.isNotEmpty) ? email : (username ?? '');
|
||||
|
||||
// Ensure we always pass a string phoneNumber to the AuthService (it expects a non-null value)
|
||||
final resolvedPhone = phoneNumber ?? '';
|
||||
|
||||
final user = await _authService.register(
|
||||
email: resolvedEmail,
|
||||
phoneNumber: resolvedPhone,
|
||||
password: password,
|
||||
);
|
||||
|
||||
_user = user;
|
||||
} finally {
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _authService.logout();
|
||||
_user = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
141
lib/features/auth/services/auth_service.dart
Normal file
141
lib/features/auth/services/auth_service.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
// lib/features/auth/services/auth_service.dart
|
||||
import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../core/api/api_client.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../../../core/storage/token_storage.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
class AuthService {
|
||||
final ApiClient _api = ApiClient();
|
||||
|
||||
/// LOGIN → returns UserModel
|
||||
Future<UserModel> login(String username, String password) async {
|
||||
try {
|
||||
final res = await _api.post(
|
||||
ApiEndpoints.login,
|
||||
body: {
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
requiresAuth: false,
|
||||
);
|
||||
|
||||
final token = res['token'];
|
||||
if (token == null) {
|
||||
throw Exception('Token missing from response');
|
||||
}
|
||||
|
||||
final serverUsername = (res['username'] is String && (res['username'] as String).isNotEmpty) ? res['username'] as String : null;
|
||||
final serverEmail = (res['email'] is String && (res['email'] as String).isNotEmpty) ? res['email'] as String : null;
|
||||
// savedEmail is the canonical identifier for this account
|
||||
final savedEmail = serverEmail ?? username;
|
||||
// candidate display name (server username or email fallback)
|
||||
final displayCandidate = serverUsername ?? savedEmail;
|
||||
|
||||
// save token (TokenStorage stays responsible for token)
|
||||
await TokenStorage.saveToken(token.toString(), savedEmail);
|
||||
|
||||
// persist per-account fields in SharedPreferences
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
// mark current logged-in account
|
||||
await prefs.setString('current_email', savedEmail);
|
||||
// always keep a canonical 'email' pointing to current user's email for old callers
|
||||
await prefs.setString('email', savedEmail);
|
||||
// save per-account display name (do not overwrite an explicit per-account display name if already set)
|
||||
final perKey = 'display_name_$savedEmail';
|
||||
final existingPer = prefs.getString(perKey);
|
||||
if (existingPer == null || existingPer.trim().isEmpty) {
|
||||
await prefs.setString(perKey, displayCandidate);
|
||||
}
|
||||
// save server-provided email (if any) separately too
|
||||
if (serverEmail != null) await prefs.setString('server_email', serverEmail);
|
||||
|
||||
// Save phone if provided (optional)
|
||||
if (res['phone_number'] != null) await prefs.setString('phone_number', res['phone_number'].toString());
|
||||
|
||||
return UserModel.fromJson(res);
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('AuthService.login error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// REGISTER → returns UserModel
|
||||
Future<UserModel> register({
|
||||
required String email,
|
||||
required String phoneNumber,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
final res = await _api.post(
|
||||
ApiEndpoints.register,
|
||||
body: {
|
||||
"email": email,
|
||||
"phone_number": phoneNumber,
|
||||
"password": password,
|
||||
},
|
||||
requiresAuth: false,
|
||||
);
|
||||
|
||||
final token = res['token'];
|
||||
if (token == null) {
|
||||
throw Exception('Token missing from response');
|
||||
}
|
||||
|
||||
final serverUsername = (res['username'] is String && (res['username'] as String).isNotEmpty) ? res['username'] as String : null;
|
||||
final serverEmail = (res['email'] is String && (res['email'] as String).isNotEmpty) ? res['email'] as String : null;
|
||||
final savedEmail = serverEmail ?? email;
|
||||
final savedUsername = serverUsername ?? savedEmail;
|
||||
final savedRole = (res['role'] ?? 'user').toString();
|
||||
final savedPhone = (res['phone_number'] ?? phoneNumber)?.toString();
|
||||
|
||||
// Save token + canonical user id for token storage
|
||||
await TokenStorage.saveToken(token.toString(), savedEmail);
|
||||
|
||||
// Persist per-account keys
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('current_email', savedEmail);
|
||||
await prefs.setString('email', savedEmail);
|
||||
|
||||
// Set display_name_<email> if not present yet
|
||||
final perKey = 'display_name_$savedEmail';
|
||||
final existing = prefs.getString(perKey);
|
||||
if (existing == null || existing.trim().isEmpty) {
|
||||
await prefs.setString(perKey, savedUsername);
|
||||
}
|
||||
|
||||
// Optionally save phone/email in prefs
|
||||
await prefs.setString('server_email', savedEmail);
|
||||
if (savedPhone != null) await prefs.setString('phone_number', savedPhone);
|
||||
|
||||
// Return a simple UserModel (defensive)
|
||||
return UserModel(
|
||||
username: savedUsername,
|
||||
email: savedEmail,
|
||||
role: savedRole,
|
||||
token: token.toString(),
|
||||
phoneNumber: savedPhone,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('AuthService.register error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Logout – clear auth token and current_email (keep per-account display_name entries so they persist)
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
// clear token storage (should delete token + auth headers)
|
||||
await TokenStorage.clear();
|
||||
// remove current_email marker so no previous account remains current
|
||||
await prefs.remove('current_email');
|
||||
// Also remove canonical 'email' pointing to current user
|
||||
await prefs.remove('email');
|
||||
// Do not delete display_name_<email> entries — they are per-account and should remain on device.
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('AuthService.logout warning: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
104
lib/features/events/models/event_models.dart
Normal file
104
lib/features/events/models/event_models.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
// lib/features/events/models/event_models.dart
|
||||
class EventTypeModel {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? iconUrl;
|
||||
|
||||
EventTypeModel({required this.id, required this.name, this.iconUrl});
|
||||
|
||||
factory EventTypeModel.fromJson(Map<String, dynamic> j) {
|
||||
return EventTypeModel(
|
||||
id: j['id'] as int,
|
||||
name: (j['event_type'] ?? j['name'] ?? '') as String,
|
||||
iconUrl: (j['event_type_icon'] ?? j['icon_url']) as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventImageModel {
|
||||
final bool isPrimary;
|
||||
final String image;
|
||||
|
||||
EventImageModel({required this.isPrimary, required this.image});
|
||||
|
||||
factory EventImageModel.fromJson(Map<String, dynamic> j) {
|
||||
return EventImageModel(
|
||||
isPrimary: j['is_primary'] == true,
|
||||
image: (j['image'] ?? '') as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventModel {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? title;
|
||||
final String? description;
|
||||
final String startDate; // YYYY-MM-DD
|
||||
final String endDate;
|
||||
final String? startTime;
|
||||
final String? endTime;
|
||||
final String? pincode;
|
||||
final String? place;
|
||||
final bool isBookable;
|
||||
final int? eventTypeId;
|
||||
final String? thumbImg;
|
||||
final List<EventImageModel> images;
|
||||
|
||||
// NEW fields mapped from backend
|
||||
final String? importantInformation;
|
||||
final String? venueName;
|
||||
final String? eventStatus;
|
||||
final String? cancelledReason;
|
||||
|
||||
EventModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.title,
|
||||
this.description,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.pincode,
|
||||
this.place,
|
||||
this.isBookable = true,
|
||||
this.eventTypeId,
|
||||
this.thumbImg,
|
||||
this.images = const [],
|
||||
this.importantInformation,
|
||||
this.venueName,
|
||||
this.eventStatus,
|
||||
this.cancelledReason,
|
||||
});
|
||||
|
||||
factory EventModel.fromJson(Map<String, dynamic> j) {
|
||||
final imgs = <EventImageModel>[];
|
||||
if (j['images'] is List) {
|
||||
for (final im in j['images']) {
|
||||
if (im is Map<String, dynamic>) imgs.add(EventImageModel.fromJson(im));
|
||||
}
|
||||
}
|
||||
|
||||
return EventModel(
|
||||
id: j['id'] is int ? j['id'] as int : int.parse(j['id'].toString()),
|
||||
name: (j['name'] ?? '') as String,
|
||||
title: j['title'] as String?,
|
||||
description: j['description'] as String?,
|
||||
startDate: (j['start_date'] ?? '') as String,
|
||||
endDate: (j['end_date'] ?? '') as String,
|
||||
startTime: j['start_time'] as String?,
|
||||
endTime: j['end_time'] as String?,
|
||||
pincode: j['pincode'] as String?,
|
||||
place: (j['place'] ?? j['venue_name']) as String?,
|
||||
isBookable: j['is_bookable'] == null ? true : (j['is_bookable'] == true || j['is_bookable'].toString().toLowerCase() == 'true'),
|
||||
eventTypeId: j['event_type'] is int ? j['event_type'] as int : (j['event_type'] != null ? int.tryParse(j['event_type'].toString()) : null),
|
||||
thumbImg: j['thumb_img'] as String?,
|
||||
images: imgs,
|
||||
importantInformation: j['important_information'] as String?,
|
||||
venueName: j['venue_name'] as String?,
|
||||
eventStatus: j['event_status'] as String?,
|
||||
cancelledReason: j['cancelled_reason'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
70
lib/features/events/services/events_service.dart
Normal file
70
lib/features/events/services/events_service.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
// lib/features/events/services/events_service.dart
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../core/api/api_client.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../models/event_models.dart';
|
||||
|
||||
class EventsService {
|
||||
final ApiClient _api = ApiClient();
|
||||
|
||||
/// Get event types (POST to /events/type-list/)
|
||||
Future<List<EventTypeModel>> getEventTypes() async {
|
||||
final res = await _api.post(ApiEndpoints.eventTypes);
|
||||
final list = <EventTypeModel>[];
|
||||
final data = res['event_types'] ?? res['event_types'] ?? res;
|
||||
if (data is List) {
|
||||
for (final e in data) {
|
||||
if (e is Map<String, dynamic>) list.add(EventTypeModel.fromJson(e));
|
||||
}
|
||||
} else if (res['event_types'] is List) {
|
||||
for (final e in res['event_types']) {
|
||||
list.add(EventTypeModel.fromJson(Map<String, dynamic>.from(e)));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Get events filtered by pincode (POST to /events/pincode-events/)
|
||||
/// Use pincode='all' to fetch all events.
|
||||
Future<List<EventModel>> getEventsByPincode(String pincode) async {
|
||||
final res = await _api.post(ApiEndpoints.eventsByPincode, body: {'pincode': pincode});
|
||||
final list = <EventModel>[];
|
||||
final events = res['events'] ?? res['data'] ?? [];
|
||||
if (events is List) {
|
||||
for (final e in events) {
|
||||
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Event details
|
||||
Future<EventModel> getEventDetails(int eventId) async {
|
||||
final res = await _api.post(ApiEndpoints.eventDetails, body: {'event_id': eventId});
|
||||
return EventModel.fromJson(Map<String, dynamic>.from(res));
|
||||
}
|
||||
|
||||
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
||||
/// Accepts month string and year int.
|
||||
/// Returns Map with 'dates' (list of YYYY-MM-DD) and 'date_events' (list with counts).
|
||||
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
|
||||
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year});
|
||||
// expected keys: dates, total_number_of_events, date_events
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Convenience: get events for a specific date (YYYY-MM-DD)
|
||||
Future<List<EventModel>> getEventsForDate(String date) async {
|
||||
// Simplest approach: hit pincode-events with filter or hit events-by-month-year and then
|
||||
// query event-details for events of that date. Assuming backend doesn't provide direct endpoint,
|
||||
// we'll call eventsByPincode('all') and filter locally by date — acceptable for demo/small datasets.
|
||||
final all = await getEventsByPincode('all');
|
||||
return all.where((e) {
|
||||
try {
|
||||
return e.startDate == date || e.endDate == date || (DateTime.parse(e.startDate).isBefore(DateTime.parse(date)) && DateTime.parse(e.endDate).isAfter(DateTime.parse(date)));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user