b239ae3e5f
Replaces Firebase with a self-hosted PHP/MySQL API served from winded.prymsolutions.com. Includes full backend (schema, auth, events, teams, brackets, suggestions, stats, media, file upload) and updated Flutter repositories and domain models. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
123 lines
3.4 KiB
Dart
123 lines
3.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
part 'api_client.g.dart';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Configuration — set this to your Hostinger domain before building.
|
|
// ---------------------------------------------------------------------------
|
|
const String kApiBase = 'https://winded.prymsolutions.com/api';
|
|
|
|
const String _tokenKey = 'winded_auth_token';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ApiClient
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class ApiClient {
|
|
ApiClient(this._storage);
|
|
|
|
final FlutterSecureStorage _storage;
|
|
|
|
// --- Token management ---
|
|
|
|
Future<String?> get token => _storage.read(key: _tokenKey);
|
|
|
|
Future<void> saveToken(String token) =>
|
|
_storage.write(key: _tokenKey, value: token);
|
|
|
|
Future<void> clearToken() => _storage.delete(key: _tokenKey);
|
|
|
|
// --- HTTP helpers ---
|
|
|
|
Future<Map<String, String>> _headers({bool auth = true}) async {
|
|
final headers = <String, String>{'Content-Type': 'application/json'};
|
|
if (auth) {
|
|
final t = await token;
|
|
if (t != null) headers['Authorization'] = 'Bearer $t';
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
Uri _uri(String path, [Map<String, String>? params]) {
|
|
final uri = Uri.parse('$kApiBase$path');
|
|
return params != null ? uri.replace(queryParameters: params) : uri;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> get(
|
|
String path, {
|
|
Map<String, String>? params,
|
|
bool auth = true,
|
|
}) async {
|
|
final res = await http.get(_uri(path, params), headers: await _headers(auth: auth));
|
|
return _parse(res);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> post(
|
|
String path,
|
|
Map<String, dynamic> body, {
|
|
bool auth = true,
|
|
}) async {
|
|
final res = await http.post(
|
|
_uri(path),
|
|
headers: await _headers(auth: auth),
|
|
body: jsonEncode(body),
|
|
);
|
|
return _parse(res);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> put(
|
|
String path,
|
|
Map<String, dynamic> body, {
|
|
Map<String, String>? params,
|
|
}) async {
|
|
final res = await http.put(
|
|
_uri(path, params),
|
|
headers: await _headers(),
|
|
body: jsonEncode(body),
|
|
);
|
|
return _parse(res);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> delete(
|
|
String path, {
|
|
Map<String, String>? params,
|
|
}) async {
|
|
final res = await http.delete(_uri(path, params), headers: await _headers());
|
|
return _parse(res);
|
|
}
|
|
|
|
Map<String, dynamic> _parse(http.Response res) {
|
|
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
if (res.statusCode >= 400) {
|
|
throw ApiException(
|
|
message: (body['error'] as String?) ?? 'Unknown error',
|
|
statusCode: res.statusCode,
|
|
);
|
|
}
|
|
return body;
|
|
}
|
|
}
|
|
|
|
class ApiException implements Exception {
|
|
const ApiException({required this.message, required this.statusCode});
|
|
final String message;
|
|
final int statusCode;
|
|
|
|
@override
|
|
String toString() => 'ApiException($statusCode): $message';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Providers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@Riverpod(keepAlive: true)
|
|
ApiClient apiClient(ApiClientRef ref) {
|
|
return ApiClient(const FlutterSecureStorage());
|
|
}
|