website refactor
This commit is contained in:
@@ -1,166 +1,130 @@
|
||||
import { ApiClient } from '@/lib/api';
|
||||
import type { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
||||
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
||||
import type { LeagueMembershipDTO } from '@/lib/types/generated/LeagueMembershipDTO';
|
||||
|
||||
let cachedLeaguesApiClient: LeaguesApiClient | undefined;
|
||||
|
||||
function getDefaultLeaguesApiClient(): LeaguesApiClient {
|
||||
if (cachedLeaguesApiClient) return cachedLeaguesApiClient;
|
||||
|
||||
const api = new ApiClient(getWebsiteApiBaseUrl());
|
||||
cachedLeaguesApiClient = api.leagues;
|
||||
return cachedLeaguesApiClient;
|
||||
export interface LeagueRosterAdminData {
|
||||
leagueId: string;
|
||||
members: LeagueRosterMemberDTO[];
|
||||
joinRequests: LeagueRosterJoinRequestDTO[];
|
||||
}
|
||||
|
||||
export class LeagueMembershipService {
|
||||
// In-memory cache for memberships (populated via API calls)
|
||||
private static leagueMemberships = new Map<string, LeagueMembership[]>();
|
||||
export class LeagueMembershipService implements Service {
|
||||
private apiClient: LeaguesApiClient;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static cachedMemberships = new Map<string, any[]>();
|
||||
|
||||
constructor(private readonly leaguesApiClient?: LeaguesApiClient) {}
|
||||
|
||||
private getClient(): LeaguesApiClient {
|
||||
return this.leaguesApiClient ?? getDefaultLeaguesApiClient();
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsDTO> {
|
||||
const dto = await this.getClient().getMemberships(leagueId);
|
||||
return dto;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getMembership(leagueId: string, driverId: string): any | null {
|
||||
const members = this.cachedMemberships.get(leagueId);
|
||||
if (!members) return null;
|
||||
return members.find(m => m.driverId === driverId) || null;
|
||||
}
|
||||
|
||||
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
|
||||
return this.getClient().removeMember(leagueId, performerDriverId, targetDriverId);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getLeagueMembers(leagueId: string): any[] {
|
||||
return this.cachedMemberships.get(leagueId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific membership from cache.
|
||||
*/
|
||||
static getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
||||
const list = this.leagueMemberships.get(leagueId);
|
||||
if (!list) return null;
|
||||
return list.find((m) => m.driverId === driverId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of a league from cache.
|
||||
*/
|
||||
static getLeagueMembers(leagueId: string): LeagueMembership[] {
|
||||
return [...(this.leagueMemberships.get(leagueId) ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache memberships for a league via API.
|
||||
*/
|
||||
static async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
|
||||
try {
|
||||
const result = await getDefaultLeaguesApiClient().getMemberships(leagueId);
|
||||
const memberships: LeagueMembership[] = (result.members ?? []).map((member) => ({
|
||||
id: `${member.driverId}-${leagueId}`, // Generate ID since API doesn't provide it
|
||||
leagueId,
|
||||
driverId: member.driverId,
|
||||
role: member.role as 'owner' | 'admin' | 'steward' | 'member',
|
||||
status: 'active', // Assume active since API returns current members
|
||||
joinedAt: member.joinedAt,
|
||||
}));
|
||||
this.setLeagueMemberships(leagueId, memberships);
|
||||
return memberships;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch league memberships:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a league.
|
||||
*
|
||||
* NOTE: The API currently exposes membership mutations via league member management endpoints.
|
||||
* For now we keep the website decoupled by consuming only the API through this service.
|
||||
*/
|
||||
async joinLeague(leagueId: string, driverId: string): Promise<void> {
|
||||
// Temporary: no join endpoint exposed yet in API.
|
||||
// Keep behavior predictable for UI.
|
||||
throw new Error('Joining leagues is not available in this build.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave a league.
|
||||
*/
|
||||
async leaveLeague(leagueId: string, driverId: string): Promise<void> {
|
||||
// Temporary: no leave endpoint exposed yet in API.
|
||||
throw new Error('Leaving leagues is not available in this build.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set memberships in cache (for use after API calls).
|
||||
*/
|
||||
static setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
|
||||
this.leagueMemberships.set(leagueId, memberships);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached memberships for a league.
|
||||
*/
|
||||
static clearLeagueMemberships(leagueId: string): void {
|
||||
this.leagueMemberships.delete(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get iterator for cached memberships (for utility functions).
|
||||
*/
|
||||
static getCachedMembershipsIterator(): IterableIterator<[string, LeagueMembership[]]> {
|
||||
return this.leagueMemberships.entries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all memberships for a specific driver across all leagues.
|
||||
*/
|
||||
static getAllMembershipsForDriver(driverId: string): LeagueMembership[] {
|
||||
const allMemberships: LeagueMembership[] = [];
|
||||
for (const [leagueId, memberships] of this.leagueMemberships.entries()) {
|
||||
const driverMembership = memberships.find((m) => m.driverId === driverId);
|
||||
if (driverMembership) {
|
||||
allMemberships.push(driverMembership);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getAllMembershipsForDriver(driverId: string): any[] {
|
||||
const allMemberships: any[] = [];
|
||||
for (const [leagueId, members] of this.cachedMemberships.entries()) {
|
||||
const membership = members.find(m => m.driverId === driverId);
|
||||
if (membership) {
|
||||
allMemberships.push({ ...membership, leagueId });
|
||||
}
|
||||
}
|
||||
return allMemberships;
|
||||
}
|
||||
|
||||
// Instance methods that delegate to static methods for consistency with service pattern
|
||||
async fetchLeagueMemberships(leagueId: string): Promise<void> {
|
||||
try {
|
||||
const members = await this.apiClient.getMemberships(leagueId);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
LeagueMembershipService.cachedMemberships.set(leagueId, (members as any).members || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch memberships', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific membership from cache.
|
||||
*/
|
||||
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getMembership(leagueId: string, driverId: string): any | null {
|
||||
return LeagueMembershipService.getMembership(leagueId, driverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of a league from cache.
|
||||
*/
|
||||
getLeagueMembers(leagueId: string): LeagueMembership[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getLeagueMembers(leagueId: string): any[] {
|
||||
return LeagueMembershipService.getLeagueMembers(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache memberships for a league via API.
|
||||
*/
|
||||
async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
|
||||
return LeagueMembershipService.fetchLeagueMemberships(leagueId);
|
||||
async joinLeague(_: string, __: string): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'joinLeague' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set memberships in cache (for use after API calls).
|
||||
*/
|
||||
setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
|
||||
LeagueMembershipService.setLeagueMemberships(leagueId, memberships);
|
||||
async leaveLeague(_: string, __: string): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'leaveLeague' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached memberships for a league.
|
||||
*/
|
||||
clearLeagueMemberships(leagueId: string): void {
|
||||
LeagueMembershipService.clearLeagueMemberships(leagueId);
|
||||
async getRosterAdminData(leagueId: string): Promise<Result<LeagueRosterAdminData, DomainError>> {
|
||||
try {
|
||||
const [members, joinRequests] = await Promise.all([
|
||||
this.apiClient.getAdminRosterMembers(leagueId),
|
||||
this.apiClient.getAdminRosterJoinRequests(leagueId),
|
||||
]);
|
||||
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueMembershipService.getRosterAdminData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster data' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async removeMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
|
||||
}
|
||||
}
|
||||
|
||||
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto';
|
||||
|
||||
export class LeagueRulebookService implements Service {
|
||||
async getRulebookData(leagueId: string): Promise<Result<RulebookApiDto, never>> {
|
||||
async getRulebookData(leagueId: string): Promise<Result<RulebookApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: RulebookApiDto = {
|
||||
leagueId,
|
||||
@@ -27,4 +27,4 @@ export class LeagueRulebookService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { LeagueScheduleApiDto } from '@/lib/types/tbd/LeagueScheduleApiDto';
|
||||
|
||||
export class LeagueScheduleService implements Service {
|
||||
async getScheduleData(leagueId: string): Promise<Result<LeagueScheduleApiDto, never>> {
|
||||
async getScheduleData(leagueId: string): Promise<Result<LeagueScheduleApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueScheduleApiDto = {
|
||||
leagueId,
|
||||
@@ -36,4 +36,4 @@ export class LeagueScheduleService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,27 @@ import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { getWebsiteServerEnv } from '@/lib/config/env';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/AllLeaguesWithCapacityAndScoringDTO';
|
||||
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
||||
|
||||
export interface LeagueScheduleAdminData {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
seasons: LeagueSeasonSummaryDTO[];
|
||||
schedule: LeagueScheduleDTO;
|
||||
}
|
||||
|
||||
export interface LeagueRosterAdminData {
|
||||
leagueId: string;
|
||||
members: LeagueRosterMemberDTO[];
|
||||
joinRequests: LeagueRosterJoinRequestDTO[];
|
||||
}
|
||||
|
||||
export interface LeagueDetailData {
|
||||
league: LeagueWithCapacityAndScoringDTO;
|
||||
apiDto: AllLeaguesWithCapacityAndScoringDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* League Service - DTO Only
|
||||
@@ -40,11 +59,10 @@ export class LeagueService implements Service {
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const { NODE_ENV } = getWebsiteServerEnv();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: NODE_ENV === 'production',
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
// Optional clients can be initialized if needed
|
||||
@@ -54,9 +72,73 @@ export class LeagueService implements Service {
|
||||
try {
|
||||
const dto = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getAllLeagues failed:', error);
|
||||
return Result.err({ type: 'serverError', message: 'Failed to fetch leagues' });
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch leagues' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueDetailData(leagueId: string): Promise<Result<LeagueDetailData, DomainError>> {
|
||||
try {
|
||||
const apiDto = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
|
||||
if (!apiDto || !apiDto.leagues) {
|
||||
return Result.err({ type: 'notFound', message: 'Leagues not found' });
|
||||
}
|
||||
|
||||
const league = apiDto.leagues.find(l => l.id === leagueId);
|
||||
if (!league) {
|
||||
return Result.err({ type: 'notFound', message: 'League not found' });
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
league,
|
||||
apiDto,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getLeagueDetailData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league detail' });
|
||||
}
|
||||
}
|
||||
|
||||
async getScheduleAdminData(leagueId: string, seasonId?: string): Promise<Result<LeagueScheduleAdminData, DomainError>> {
|
||||
try {
|
||||
const seasons = await this.apiClient.getSeasons(leagueId);
|
||||
|
||||
if (!seasons || seasons.length === 0) {
|
||||
return Result.err({ type: 'notFound', message: 'No seasons found for league' });
|
||||
}
|
||||
|
||||
const targetSeasonId = seasonId || (seasons.find(s => s.status === 'active')?.seasonId || seasons[0].seasonId);
|
||||
const schedule = await this.apiClient.getSchedule(leagueId, targetSeasonId);
|
||||
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
seasonId: targetSeasonId,
|
||||
seasons,
|
||||
schedule,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getScheduleAdminData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch schedule admin data' });
|
||||
}
|
||||
}
|
||||
|
||||
async getRosterAdminData(leagueId: string): Promise<Result<LeagueRosterAdminData, DomainError>> {
|
||||
try {
|
||||
const [members, joinRequests] = await Promise.all([
|
||||
this.apiClient.getAdminRosterMembers(leagueId),
|
||||
this.apiClient.getAdminRosterJoinRequests(leagueId),
|
||||
]);
|
||||
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getRosterAdminData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster data' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,41 +146,76 @@ export class LeagueService implements Service {
|
||||
return Result.err({ type: 'notImplemented', message: 'League standings endpoint not implemented' });
|
||||
}
|
||||
|
||||
async getLeagueStats(): Promise<TotalLeaguesDTO> {
|
||||
return this.apiClient.getTotal();
|
||||
async getLeagueStats(): Promise<Result<TotalLeaguesDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getTotal();
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league stats' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDTO> {
|
||||
return this.apiClient.getSchedule(leagueId);
|
||||
async getLeagueSchedule(leagueId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getSchedule(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueSeasons(leagueId: string): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
return this.apiClient.getSeasons(leagueId);
|
||||
async getLeagueSeasons(leagueId: string): Promise<Result<LeagueSeasonSummaryDTO[], DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getSeasons(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league seasons' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueSeasonSummaries(leagueId: string): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
return this.apiClient.getSeasons(leagueId);
|
||||
async getLeagueSeasonSummaries(leagueId: string): Promise<Result<LeagueSeasonSummaryDTO[], DomainError>> {
|
||||
return this.getLeagueSeasons(leagueId);
|
||||
}
|
||||
|
||||
async getAdminSchedule(leagueId: string, seasonId: string): Promise<LeagueScheduleDTO> {
|
||||
return this.apiClient.getSchedule(leagueId, seasonId);
|
||||
async getAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getSchedule(leagueId, seasonId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch admin schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async publishAdminSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.publishSeasonSchedule(leagueId, seasonId);
|
||||
async publishAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.publishSeasonSchedule(leagueId, seasonId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to publish schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async unpublishAdminSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.unpublishSeasonSchedule(leagueId, seasonId);
|
||||
async unpublishAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.unpublishSeasonSchedule(leagueId, seasonId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to unpublish schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async createAdminScheduleRace(
|
||||
leagueId: string,
|
||||
seasonId: string,
|
||||
input: { track: string; car: string; scheduledAtIso: string },
|
||||
): Promise<CreateLeagueScheduleRaceOutputDTO> {
|
||||
const payload: CreateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
return this.apiClient.createSeasonScheduleRace(leagueId, seasonId, payload);
|
||||
): Promise<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
|
||||
try {
|
||||
const payload: CreateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, payload);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateAdminScheduleRace(
|
||||
@@ -106,33 +223,48 @@ export class LeagueService implements Service {
|
||||
seasonId: string,
|
||||
raceId: string,
|
||||
input: Partial<{ track: string; car: string; scheduledAtIso: string }>,
|
||||
): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
const payload: UpdateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
return this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, payload);
|
||||
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
try {
|
||||
const payload: UpdateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, payload);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAdminScheduleRace(leagueId: string, seasonId: string, raceId: string): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
return this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId);
|
||||
async deleteAdminScheduleRace(leagueId: string, seasonId: string, raceId: string): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to delete race' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueScheduleDto(leagueId: string, seasonId: string): Promise<LeagueScheduleDTO> {
|
||||
return this.apiClient.getSchedule(leagueId, seasonId);
|
||||
async getLeagueScheduleDto(leagueId: string, seasonId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
||||
return this.getAdminSchedule(leagueId, seasonId);
|
||||
}
|
||||
|
||||
async publishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.publishSeasonSchedule(leagueId, seasonId);
|
||||
async publishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
return this.publishAdminSchedule(leagueId, seasonId);
|
||||
}
|
||||
|
||||
async unpublishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.unpublishSeasonSchedule(leagueId, seasonId);
|
||||
async unpublishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
return this.unpublishAdminSchedule(leagueId, seasonId);
|
||||
}
|
||||
|
||||
async createLeagueSeasonScheduleRace(
|
||||
leagueId: string,
|
||||
seasonId: string,
|
||||
input: CreateLeagueScheduleRaceInputDTO,
|
||||
): Promise<CreateLeagueScheduleRaceOutputDTO> {
|
||||
return this.apiClient.createSeasonScheduleRace(leagueId, seasonId, input);
|
||||
): Promise<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateLeagueSeasonScheduleRace(
|
||||
@@ -140,52 +272,93 @@ export class LeagueService implements Service {
|
||||
seasonId: string,
|
||||
raceId: string,
|
||||
input: UpdateLeagueScheduleRaceInputDTO,
|
||||
): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
return this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, input);
|
||||
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteLeagueSeasonScheduleRace(
|
||||
leagueId: string,
|
||||
seasonId: string,
|
||||
raceId: string,
|
||||
): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
return this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId);
|
||||
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
return this.deleteAdminScheduleRace(leagueId, seasonId, raceId);
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
|
||||
return this.apiClient.getMemberships(leagueId);
|
||||
async getLeagueMemberships(leagueId: string): Promise<Result<LeagueMembershipsDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getMemberships(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch memberships' });
|
||||
}
|
||||
}
|
||||
|
||||
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueOutputDTO> {
|
||||
return this.apiClient.create(input);
|
||||
async createLeague(input: CreateLeagueInputDTO): Promise<Result<CreateLeagueOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.create(input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create league' });
|
||||
}
|
||||
}
|
||||
|
||||
async removeMember(leagueId: string, targetDriverId: string): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
|
||||
return { success: dto.success };
|
||||
async removeMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.updateRosterMemberRole(leagueId, targetDriverId, newRole);
|
||||
return { success: dto.success };
|
||||
async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.updateRosterMemberRole(leagueId, targetDriverId, newRole);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update member role' });
|
||||
}
|
||||
}
|
||||
|
||||
async getAdminRosterMembers(leagueId: string): Promise<LeagueRosterMemberDTO[]> {
|
||||
return this.apiClient.getAdminRosterMembers(leagueId);
|
||||
async getAdminRosterMembers(leagueId: string): Promise<Result<LeagueRosterMemberDTO[], DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getAdminRosterMembers(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster members' });
|
||||
}
|
||||
}
|
||||
|
||||
async getAdminRosterJoinRequests(leagueId: string): Promise<LeagueRosterJoinRequestDTO[]> {
|
||||
return this.apiClient.getAdminRosterJoinRequests(leagueId);
|
||||
async getAdminRosterJoinRequests(leagueId: string): Promise<Result<LeagueRosterJoinRequestDTO[], DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getAdminRosterJoinRequests(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch join requests' });
|
||||
}
|
||||
}
|
||||
|
||||
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
|
||||
return { success: dto.success };
|
||||
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
|
||||
return { success: dto.success };
|
||||
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueDetail(): Promise<Result<never, DomainError>> {
|
||||
@@ -199,4 +372,4 @@ export class LeagueService implements Service {
|
||||
async getScoringPresets(): Promise<Result<never, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'Scoring presets endpoint not implemented' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { type LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
|
||||
|
||||
export class LeagueSettingsService implements Service {
|
||||
async getSettingsData(leagueId: string): Promise<Result<LeagueSettingsApiDto, never>> {
|
||||
private static cachedMemberships = new Map<string, unknown[]>();
|
||||
|
||||
async getSettingsData(leagueId: string): Promise<Result<LeagueSettingsApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueSettingsApiDto = {
|
||||
leagueId,
|
||||
@@ -25,4 +27,14 @@ export class LeagueSettingsService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
|
||||
static getCachedMembershipsIterator(): IterableIterator<[string, unknown[]]> {
|
||||
return this.cachedMemberships.entries();
|
||||
}
|
||||
|
||||
static getMembership(leagueId: string, driverId: string): unknown | null {
|
||||
const members = this.cachedMemberships.get(leagueId);
|
||||
if (!members) return null;
|
||||
return members.find((m: any) => m.driverId === driverId) || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
|
||||
|
||||
export class LeagueSponsorshipsService implements Service {
|
||||
async getSponsorshipsData(leagueId: string): Promise<Result<LeagueSponsorshipsApiDto, never>> {
|
||||
async getSponsorshipsData(leagueId: string): Promise<Result<LeagueSponsorshipsApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueSponsorshipsApiDto = {
|
||||
leagueId,
|
||||
@@ -56,4 +56,4 @@ export class LeagueSponsorshipsService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { LeagueStandingsApiDto, LeagueMembershipsApiDto } from '@/lib/types/tbd/LeagueStandingsApiDto';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { type LeagueStandingsApiDto, type LeagueMembershipsApiDto } from '@/lib/types/tbd/LeagueStandingsApiDto';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
@@ -18,7 +18,7 @@ export class LeagueStandingsService implements Service {
|
||||
);
|
||||
}
|
||||
|
||||
async getStandingsData(leagueId: string): Promise<Result<{ standings: LeagueStandingsApiDto; memberships: LeagueMembershipsApiDto }, never>> {
|
||||
async getStandingsData(_: string): Promise<Result<{ standings: LeagueStandingsApiDto; memberships: LeagueMembershipsApiDto }, DomainError>> {
|
||||
// Mock data since backend may not be implemented
|
||||
const mockStandings: LeagueStandingsApiDto = {
|
||||
standings: [
|
||||
@@ -86,4 +86,4 @@ export class LeagueStandingsService implements Service {
|
||||
|
||||
return Result.ok({ standings: mockStandings, memberships: mockMemberships });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { type StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
|
||||
|
||||
export class LeagueStewardingService implements Service {
|
||||
async getStewardingData(leagueId: string): Promise<Result<StewardingApiDto, never>> {
|
||||
async getStewardingData(leagueId: string): Promise<Result<StewardingApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: StewardingApiDto = {
|
||||
leagueId,
|
||||
@@ -15,4 +15,8 @@ export class LeagueStewardingService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
|
||||
async getProtestDetailViewModel(_: string, __: string): Promise<Result<any, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'getProtestDetailViewModel' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
|
||||
|
||||
export class LeagueWalletService implements Service {
|
||||
async getWalletData(leagueId: string): Promise<Result<LeagueWalletApiDto, never>> {
|
||||
async getWalletForLeague(leagueId: string): Promise<LeagueWalletApiDto> {
|
||||
const result = await this.getWalletData(leagueId);
|
||||
if (result.isErr()) throw new Error(result.getError().message);
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
async withdraw(
|
||||
leagueId: string,
|
||||
amount: number,
|
||||
currency: string,
|
||||
seasonId: string,
|
||||
destinationId: string
|
||||
): Promise<{ success: boolean; message?: string }> {
|
||||
// Mock implementation
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getWalletData(leagueId: string): Promise<Result<LeagueWalletApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueWalletApiDto = {
|
||||
leagueId,
|
||||
@@ -46,4 +63,4 @@ export class LeagueWalletService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ApiClient } from '@/lib/api';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
|
||||
type ProfileLeaguesServiceError = 'notFound' | 'redirect' | 'unknown';
|
||||
|
||||
interface ProfileLeaguesPageDto {
|
||||
export interface ProfileLeaguesPageDto {
|
||||
ownedLeagues: Array<{
|
||||
leagueId: string;
|
||||
name: string;
|
||||
@@ -27,7 +25,7 @@ interface MembershipDTO {
|
||||
}
|
||||
|
||||
export class ProfileLeaguesService implements Service {
|
||||
async getProfileLeagues(driverId: string): Promise<Result<ProfileLeaguesPageDto, ProfileLeaguesServiceError>> {
|
||||
async getProfileLeagues(driverId: string): Promise<Result<ProfileLeaguesPageDto, DomainError>> {
|
||||
try {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const apiClient = new ApiClient(baseUrl);
|
||||
@@ -35,7 +33,7 @@ export class ProfileLeaguesService implements Service {
|
||||
const leaguesDto = await apiClient.leagues.getAllWithCapacity();
|
||||
|
||||
if (!leaguesDto?.leagues) {
|
||||
return Result.err('notFound');
|
||||
return Result.err({ type: 'notFound', message: 'Leagues not found' });
|
||||
}
|
||||
|
||||
// Fetch all memberships in parallel
|
||||
@@ -80,18 +78,18 @@ export class ProfileLeaguesService implements Service {
|
||||
ownedLeagues,
|
||||
memberLeagues,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
|
||||
if (errorAny.statusCode === 404 || errorAny.message?.toLowerCase().includes('not found')) {
|
||||
return Result.err('notFound');
|
||||
return Result.err({ type: 'notFound', message: 'Profile leagues not found' });
|
||||
}
|
||||
|
||||
if (errorAny.statusCode === 302 || errorAny.message?.toLowerCase().includes('redirect')) {
|
||||
return Result.err('redirect');
|
||||
return Result.err({ type: 'unauthorized', message: 'Unauthorized access' });
|
||||
}
|
||||
|
||||
return Result.err('unknown');
|
||||
return Result.err({ type: 'unknown', message: error.message || 'Failed to fetch profile leagues' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { ProtestDetailApiDto } from '@/lib/types/tbd/ProtestDetailApiDto';
|
||||
|
||||
export class ProtestDetailService implements Service {
|
||||
async getProtestDetail(leagueId: string, protestId: string): Promise<Result<ProtestDetailApiDto, never>> {
|
||||
async getProtestDetail(leagueId: string, protestId: string): Promise<Result<ProtestDetailApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: ProtestDetailApiDto = {
|
||||
id: protestId,
|
||||
@@ -35,4 +35,4 @@ export class ProtestDetailService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user