Initial commit

This commit is contained in:
Lorenzo Sanesi
2026-03-26 22:46:03 +01:00
commit 18e0e04700
80 changed files with 13114 additions and 0 deletions
@@ -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();
}
}
+142
View File
@@ -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;
}
}