Initial commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Exception thrown when an API request fails
|
||||
class ApiException implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
ApiException(this.message, [this.statusCode]);
|
||||
|
||||
@override
|
||||
String toString() => 'ApiException: $message (Status: $statusCode)';
|
||||
}
|
||||
|
||||
/// HTTP API client for making network requests
|
||||
class ApiClient {
|
||||
final http.Client _httpClient;
|
||||
final String baseUrl;
|
||||
final Duration timeout;
|
||||
|
||||
ApiClient({
|
||||
http.Client? httpClient,
|
||||
required this.baseUrl,
|
||||
this.timeout = const Duration(seconds: 30),
|
||||
}) : _httpClient = httpClient ?? http.Client();
|
||||
|
||||
/// Make a GET request
|
||||
Future<Map<String, dynamic>> get(
|
||||
String endpoint, {
|
||||
Map<String, String>? headers,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
final uri = _buildUri(endpoint, queryParameters);
|
||||
|
||||
try {
|
||||
final response = await _httpClient
|
||||
.get(
|
||||
uri,
|
||||
headers: _buildHeaders(headers),
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
return _handleResponse(response);
|
||||
} on TimeoutException {
|
||||
throw ApiException('Request timeout');
|
||||
} catch (e) {
|
||||
throw ApiException('Network error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a POST request
|
||||
Future<Map<String, dynamic>> post(
|
||||
String endpoint, {
|
||||
Map<String, String>? headers,
|
||||
Map<String, dynamic>? body,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
final uri = _buildUri(endpoint, queryParameters);
|
||||
|
||||
try {
|
||||
final response = await _httpClient
|
||||
.post(
|
||||
uri,
|
||||
headers: _buildHeaders(headers),
|
||||
body: body != null ? jsonEncode(body) : null,
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
return _handleResponse(response);
|
||||
} on TimeoutException {
|
||||
throw ApiException('Request timeout');
|
||||
} catch (e) {
|
||||
throw ApiException('Network error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a PUT request
|
||||
Future<Map<String, dynamic>> put(
|
||||
String endpoint, {
|
||||
Map<String, String>? headers,
|
||||
Map<String, dynamic>? body,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
final uri = _buildUri(endpoint, queryParameters);
|
||||
|
||||
try {
|
||||
final response = await _httpClient
|
||||
.put(
|
||||
uri,
|
||||
headers: _buildHeaders(headers),
|
||||
body: body != null ? jsonEncode(body) : null,
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
return _handleResponse(response);
|
||||
} on TimeoutException {
|
||||
throw ApiException('Request timeout');
|
||||
} catch (e) {
|
||||
throw ApiException('Network error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a DELETE request
|
||||
Future<Map<String, dynamic>> delete(
|
||||
String endpoint, {
|
||||
Map<String, String>? headers,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
final uri = _buildUri(endpoint, queryParameters);
|
||||
|
||||
try {
|
||||
final response = await _httpClient
|
||||
.delete(
|
||||
uri,
|
||||
headers: _buildHeaders(headers),
|
||||
)
|
||||
.timeout(timeout);
|
||||
|
||||
return _handleResponse(response);
|
||||
} on TimeoutException {
|
||||
throw ApiException('Request timeout');
|
||||
} catch (e) {
|
||||
throw ApiException('Network error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Build URI with query parameters
|
||||
Uri _buildUri(String endpoint, Map<String, dynamic>? queryParameters) {
|
||||
final path = endpoint.startsWith('/') ? endpoint : '/$endpoint';
|
||||
final uri = Uri.parse('$baseUrl$path');
|
||||
|
||||
if (queryParameters != null && queryParameters.isNotEmpty) {
|
||||
return uri.replace(
|
||||
queryParameters: queryParameters.map(
|
||||
(key, value) => MapEntry(key, value.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/// Build request headers
|
||||
Map<String, String> _buildHeaders(Map<String, String>? customHeaders) {
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
if (customHeaders != null) {
|
||||
headers.addAll(customHeaders);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/// Handle HTTP response
|
||||
Map<String, dynamic> _handleResponse(http.Response response) {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
if (response.body.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw ApiException('Invalid JSON response');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle error responses
|
||||
String errorMessage = 'Request failed';
|
||||
try {
|
||||
final errorBody = jsonDecode(response.body);
|
||||
errorMessage = errorBody['message'] ?? errorBody['error'] ?? errorMessage;
|
||||
} catch (_) {
|
||||
errorMessage = response.body.isNotEmpty
|
||||
? response.body
|
||||
: 'Status code: ${response.statusCode}';
|
||||
}
|
||||
|
||||
throw ApiException(errorMessage, response.statusCode);
|
||||
}
|
||||
|
||||
/// Dispose resources
|
||||
void dispose() {
|
||||
_httpClient.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'user.dart';
|
||||
|
||||
enum BetStatus {
|
||||
active('ACTIVE'),
|
||||
awaitingValidation('AWAITING_VALIDATION'),
|
||||
completed('COMPLETED'),
|
||||
expired('EXPIRED');
|
||||
|
||||
final String value;
|
||||
const BetStatus(this.value);
|
||||
|
||||
static BetStatus fromString(String value) {
|
||||
return BetStatus.values.firstWhere(
|
||||
(e) => e.value == value,
|
||||
orElse: () => BetStatus.active,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum BetRole {
|
||||
participant('PARTICIPANT'),
|
||||
observer('OBSERVER');
|
||||
|
||||
final String value;
|
||||
const BetRole(this.value);
|
||||
|
||||
static BetRole fromString(String value) {
|
||||
return BetRole.values.firstWhere(
|
||||
(e) => e.value == value,
|
||||
orElse: () => BetRole.observer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BetParticipant extends Equatable {
|
||||
final User user;
|
||||
final BetRole role;
|
||||
|
||||
const BetParticipant({
|
||||
required this.user,
|
||||
required this.role,
|
||||
});
|
||||
|
||||
factory BetParticipant.fromJson(Map<String, dynamic> json) {
|
||||
return BetParticipant(
|
||||
user: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||
role: BetRole.fromString(json['role'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user': user.toJson(),
|
||||
'role': role.value,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [user, role];
|
||||
}
|
||||
|
||||
class Bet extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final List<BetParticipant> participants;
|
||||
final BetStatus status;
|
||||
final DateTime startDate;
|
||||
final DateTime? endDate;
|
||||
final bool isPublic;
|
||||
final String? groupId;
|
||||
final String stake;
|
||||
final User? winner;
|
||||
|
||||
const Bet({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.participants,
|
||||
required this.status,
|
||||
required this.startDate,
|
||||
this.endDate,
|
||||
required this.isPublic,
|
||||
this.groupId,
|
||||
required this.stake,
|
||||
this.winner,
|
||||
});
|
||||
|
||||
factory Bet.fromJson(Map<String, dynamic> json) {
|
||||
return Bet(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
participants: (json['participants'] as List)
|
||||
.map((p) => BetParticipant.fromJson(p as Map<String, dynamic>))
|
||||
.toList(),
|
||||
status: BetStatus.fromString(json['status'] as String),
|
||||
startDate: DateTime.parse(json['startDate'] as String),
|
||||
endDate: json['endDate'] != null
|
||||
? DateTime.parse(json['endDate'] as String)
|
||||
: null,
|
||||
isPublic: json['isPublic'] as bool,
|
||||
groupId: json['groupId'] as String?,
|
||||
stake: json['stake'] as String,
|
||||
winner: json['winner'] != null
|
||||
? User.fromJson(json['winner'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'participants': participants.map((p) => p.toJson()).toList(),
|
||||
'status': status.value,
|
||||
'startDate': startDate.toIso8601String(),
|
||||
'endDate': endDate?.toIso8601String(),
|
||||
'isPublic': isPublic,
|
||||
'groupId': groupId,
|
||||
'stake': stake,
|
||||
'winner': winner?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
participants,
|
||||
status,
|
||||
startDate,
|
||||
endDate,
|
||||
isPublic,
|
||||
groupId,
|
||||
stake,
|
||||
winner,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'user.dart';
|
||||
|
||||
enum FriendRequestStatus {
|
||||
pending('PENDING');
|
||||
|
||||
final String value;
|
||||
const FriendRequestStatus(this.value);
|
||||
|
||||
static FriendRequestStatus fromString(String value) {
|
||||
return FriendRequestStatus.values.firstWhere(
|
||||
(e) => e.value == value,
|
||||
orElse: () => FriendRequestStatus.pending,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FriendRequest extends Equatable {
|
||||
final String id;
|
||||
final User fromUser;
|
||||
final User toUser;
|
||||
final FriendRequestStatus status;
|
||||
final DateTime createdAt;
|
||||
|
||||
const FriendRequest({
|
||||
required this.id,
|
||||
required this.fromUser,
|
||||
required this.toUser,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory FriendRequest.fromJson(Map<String, dynamic> json) {
|
||||
return FriendRequest(
|
||||
id: json['id'] as String,
|
||||
fromUser: User.fromJson(json['fromUser'] as Map<String, dynamic>),
|
||||
toUser: User.fromJson(json['toUser'] as Map<String, dynamic>),
|
||||
status: FriendRequestStatus.fromString(json['status'] as String),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'fromUser': fromUser.toJson(),
|
||||
'toUser': toUser.toJson(),
|
||||
'status': status.value,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, fromUser, toUser, status, createdAt];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'user.dart';
|
||||
|
||||
class Group extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final List<User> members;
|
||||
|
||||
const Group({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.members,
|
||||
});
|
||||
|
||||
factory Group.fromJson(Map<String, dynamic> json) {
|
||||
return Group(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
members: (json['members'] as List)
|
||||
.map((m) => User.fromJson(m as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'members': members.map((m) => m.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
Group copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
List<User>? members,
|
||||
}) {
|
||||
return Group(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
members: members ?? this.members,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, members];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class User extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String email;
|
||||
final String avatarUrl;
|
||||
final double score;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.email,
|
||||
required this.avatarUrl,
|
||||
required this.score,
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
email: json['email'] as String,
|
||||
avatarUrl: json['avatarUrl'] as String,
|
||||
score: (json['score'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'email': email,
|
||||
'avatarUrl': avatarUrl,
|
||||
'score': score,
|
||||
};
|
||||
}
|
||||
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? email,
|
||||
String? avatarUrl,
|
||||
double? score,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
email: email ?? this.email,
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
score: score ?? this.score,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, email, avatarUrl, score];
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// App route paths
|
||||
class AppRoutes {
|
||||
static const String home = '/';
|
||||
static const String matchDetails = '/match/:id';
|
||||
static const String bookmakerDetails = '/bookmaker/:id';
|
||||
static const String settings = '/settings';
|
||||
}
|
||||
|
||||
/// Route configuration for Navigator 2.0
|
||||
class AppRouterDelegate extends RouterDelegate<RouteInformation>
|
||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteInformation> {
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
String _currentPath = AppRoutes.home;
|
||||
String? _matchId;
|
||||
String? _bookmakerId;
|
||||
|
||||
AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
String get currentPath => _currentPath;
|
||||
|
||||
@override
|
||||
RouteInformation? get currentConfiguration {
|
||||
if (_matchId != null) {
|
||||
return RouteInformation(
|
||||
uri: Uri.parse('/match/$_matchId'),
|
||||
);
|
||||
}
|
||||
if (_bookmakerId != null) {
|
||||
return RouteInformation(
|
||||
uri: Uri.parse('/bookmaker/$_bookmakerId'),
|
||||
);
|
||||
}
|
||||
return RouteInformation(uri: Uri.parse(_currentPath));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(RouteInformation configuration) async {
|
||||
final uri = configuration.uri;
|
||||
|
||||
if (uri.pathSegments.isEmpty) {
|
||||
_currentPath = AppRoutes.home;
|
||||
_matchId = null;
|
||||
_bookmakerId = null;
|
||||
} else if (uri.pathSegments.length == 2) {
|
||||
if (uri.pathSegments[0] == 'match') {
|
||||
_currentPath = AppRoutes.matchDetails;
|
||||
_matchId = uri.pathSegments[1];
|
||||
} else if (uri.pathSegments[0] == 'bookmaker') {
|
||||
_currentPath = AppRoutes.bookmakerDetails;
|
||||
_bookmakerId = uri.pathSegments[1];
|
||||
}
|
||||
} else if (uri.pathSegments.length == 1) {
|
||||
if (uri.pathSegments[0] == 'settings') {
|
||||
_currentPath = AppRoutes.settings;
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
pages: [
|
||||
const MaterialPage(
|
||||
key: ValueKey('HomePage'),
|
||||
child: Placeholder(), // Replace with actual HomePage
|
||||
),
|
||||
if (_matchId != null)
|
||||
MaterialPage(
|
||||
key: ValueKey('MatchDetailsPage-$_matchId'),
|
||||
child: Placeholder(), // Replace with MatchDetailsPage
|
||||
),
|
||||
if (_bookmakerId != null)
|
||||
MaterialPage(
|
||||
key: ValueKey('BookmakerDetailsPage-$_bookmakerId'),
|
||||
child: Placeholder(), // Replace with BookmakerDetailsPage
|
||||
),
|
||||
if (_currentPath == AppRoutes.settings)
|
||||
const MaterialPage(
|
||||
key: ValueKey('SettingsPage'),
|
||||
child: Placeholder(), // Replace with SettingsPage
|
||||
),
|
||||
],
|
||||
onPopPage: (route, result) {
|
||||
if (!route.didPop(result)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle navigation state after pop
|
||||
if (_matchId != null) {
|
||||
_matchId = null;
|
||||
notifyListeners();
|
||||
} else if (_bookmakerId != null) {
|
||||
_bookmakerId = null;
|
||||
notifyListeners();
|
||||
} else if (_currentPath == AppRoutes.settings) {
|
||||
_currentPath = AppRoutes.home;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Navigate to match details
|
||||
void navigateToMatchDetails(String matchId) {
|
||||
_matchId = matchId;
|
||||
_currentPath = AppRoutes.matchDetails;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Navigate to bookmaker details
|
||||
void navigateToBookmakerDetails(String bookmakerId) {
|
||||
_bookmakerId = bookmakerId;
|
||||
_currentPath = AppRoutes.bookmakerDetails;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Navigate to settings
|
||||
void navigateToSettings() {
|
||||
_currentPath = AppRoutes.settings;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Navigate to home
|
||||
void navigateToHome() {
|
||||
_currentPath = AppRoutes.home;
|
||||
_matchId = null;
|
||||
_bookmakerId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Route information parser for Navigator 2.0
|
||||
class AppRouteInformationParser extends RouteInformationParser<RouteInformation> {
|
||||
@override
|
||||
Future<RouteInformation> parseRouteInformation(
|
||||
RouteInformation routeInformation,
|
||||
) async {
|
||||
return routeInformation;
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation? restoreRouteInformation(RouteInformation configuration) {
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user