Files
philip b239ae3e5f Initial commit: Flutter app + PHP/MySQL backend on Hostinger
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>
2026-05-14 20:13:57 -07:00

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());
}