🔧 Refactor Challonge client and tournament logic to use reusable hooks, reducing code duplication and improving maintainability

This commit is contained in:
2026-01-29 06:27:41 +00:00
parent 1eb61c2a4b
commit 96a9c07184

View File

@@ -250,6 +250,13 @@ import TournamentDetail from '../components/challonge/TournamentDetail.vue';
import { ScopeType } from '../services/challonge.service.js';
const { getApiKey } = useChallongeApiKey();
const apiKey = computed(() => getApiKey());
const maskedApiKey = computed(() => {
if (!apiKey.value) return '';
return apiKey.value.slice(0, 4) + '•••••••' + apiKey.value.slice(-4);
});
// OAuth Management
const {
isAuthenticated,
accessToken,
@@ -257,36 +264,19 @@ const {
logout: oauthLogout,
loading: oauthLoading
} = useChallongeOAuth();
// Client Credentials Management
const {
isAuthenticated: isClientCredsAuthenticated,
accessToken: clientCredsToken
} = useChallongeClientCredentials();
// API Configuration
const apiVersion = ref('v2.1'); // 'v1' or 'v2.1'
// State management with useAsyncState
const tournamentListState = useAsyncState();
const loadMoreState = useAsyncState();
const tournamentDetailsState = useAsyncState();
// Destructure for template usage
const { data: tournaments, isLoading: loading, error } = tournamentListState;
const { isLoading: loadingMore } = loadMoreState;
const searchQuery = ref('');
const expandedTournamentId = ref(null);
// Pagination
const currentPage = ref(1);
const perPage = ref(10);
const totalTournaments = ref(0);
const hasNextPage = ref(false);
// v2.1 Tournament Scope
const apiVersion = ref('v2.1');
const tournamentScope = ref(ScopeType.USER);
const perPage = ref(100);
// Debug mode - enabled to see what's happening
// Debug mode
const debugMode = ref(true);
// Collapsible section states
@@ -294,249 +284,56 @@ const apiKeyCollapsed = ref(false);
const oauthCollapsed = ref(false);
const clientCredsCollapsed = ref(false);
// Make apiKey reactive
const apiKey = computed(() => getApiKey());
// Initialize Challonge Client (replaces ~100 lines of inline client creation)
const {
client,
apiVersion: clientApiVersion,
tournamentScope: clientTournamentScope,
maskedApiKey: clientMaskedApiKey,
authType
} = useChallongeClient(
apiKey,
apiVersion,
tournamentScope,
{
oauthToken: accessToken,
oauthAuthenticated: isAuthenticated,
clientCredsToken: clientCredsToken,
clientCredsAuthenticated: isClientCredsAuthenticated
},
debugMode
);
const maskedApiKey = computed(() => {
if (!apiKey.value) return '';
return apiKey.value.slice(0, 4) + '•••••••' + apiKey.value.slice(-4);
// Initialize Tournament Tests (replaces ~200 lines of tournament logic)
const {
tournaments,
loading,
loadingMore,
error,
searchQuery,
expandedTournamentId,
currentPage,
hasNextPage,
tournamentDetails,
paginationInfo,
filteredTournaments,
testListTournaments,
loadMoreTournaments,
changePerPage,
toggleTournamentDetails,
resetState,
tournamentDetailsState
} = useChallongeTests(client, apiVersion, tournamentScope);
// Update perPage when selector changes
watch(perPage, (newValue) => {
changePerPage(newValue);
});
// Tournament details computed from async state
const tournamentDetails = computed(() => tournamentDetailsState.data.value);
// Create API client reactively based on version, key, and OAuth status
const client = computed(() => {
if (apiVersion.value === 'v1') {
// v1 only supports API key
if (!apiKey.value) return null;
return createChallongeV1Client(apiKey.value);
} else {
// v2.1 supports OAuth, client credentials, and API key
// Smart priority based on scope selection:
// - APPLICATION scope: prefer client credentials (they're designed for this)
// - USER scope: prefer OAuth user tokens or API key (client creds don't work well here)
if (tournamentScope.value === ScopeType.APPLICATION) {
// APPLICATION scope - prefer client credentials
if (isClientCredsAuthenticated.value && clientCredsToken.value) {
console.log('🔐 Using Client Credentials token for APPLICATION scope');
return createChallongeV2Client(
{ token: clientCredsToken.value, type: AuthType.OAUTH },
{ debug: debugMode.value }
);
} else if (isAuthenticated.value && accessToken.value) {
console.log('🔐 Using OAuth user token for APPLICATION scope');
return createChallongeV2Client(
{ token: accessToken.value, type: AuthType.OAUTH },
{ debug: debugMode.value }
);
}
} else {
// USER scope - prefer OAuth user tokens or API key (client creds may not work)
if (isAuthenticated.value && accessToken.value) {
console.log('🔐 Using OAuth user token for USER scope');
return createChallongeV2Client(
{ token: accessToken.value, type: AuthType.OAUTH },
{ debug: debugMode.value }
);
} else if (apiKey.value) {
console.log('🔑 Using API Key for USER scope');
return createChallongeV2Client(
{ token: apiKey.value, type: AuthType.API_KEY },
{ debug: debugMode.value }
);
}
}
// Fallback: try API key
if (apiKey.value) {
console.log('🔑 Using API Key (fallback)');
return createChallongeV2Client(
{ token: apiKey.value, type: AuthType.API_KEY },
{ debug: debugMode.value }
);
}
return null;
}
});
// Pagination info
const paginationInfo = computed(() => {
if (!tournaments.value) return '';
const start = (currentPage.value - 1) * perPage.value + 1;
const end = Math.min(
start + tournaments.value.length - 1,
totalTournaments.value || tournaments.value.length
);
const total = totalTournaments.value || tournaments.value.length;
return `Showing ${start}-${end} of ${total}`;
});
// Filter tournaments (client-side for now, can be moved to server-side)
const filteredTournaments = computed(() => {
if (!tournaments.value) return null;
if (!searchQuery.value.trim()) return tournaments.value;
const query = searchQuery.value.toLowerCase();
return tournaments.value.filter(t => {
const name = getTournamentName(t).toLowerCase();
return name.includes(query);
});
});
// Helper to get tournament name (handles both v1 and v2.1 response structures)
function getTournamentName(tournament) {
return tournament.tournament?.name || tournament.name || '';
}
// Helper to get tournament ID
function getTournamentId(tournament) {
return tournament.tournament?.id || tournament.id;
}
// Helper to get tournament property
function getTournamentProp(tournament, prop) {
return tournament.tournament?.[prop] || tournament[prop];
}
async function testListTournaments(resetPagination = true) {
if (resetPagination) {
currentPage.value = 1;
searchQuery.value = '';
expandedTournamentId.value = null;
tournamentDetailsState.reset();
}
await tournamentListState.execute(async () => {
if (apiVersion.value === 'v1') {
// v1 doesn't support pagination
const result = await client.value.tournaments.list();
totalTournaments.value = result.length;
hasNextPage.value = false;
return result;
} else {
// v2.1 - Query all tournament states (pending, in_progress, ended) in parallel
// USER scope returns tournaments you have access to:
// - Tournaments you created
// - Tournaments where you're added as an admin
const result = await queryAllTournaments(client.value, {
page: currentPage.value,
per_page: 100,
scopeType: tournamentScope.value
});
console.log('📊 Tournament API Response (All States):', {
page: currentPage.value,
perPage: 100,
scope: tournamentScope.value,
scopeRaw: tournamentScope.value,
ScopeType_USER: ScopeType.USER,
ScopeType_APPLICATION: ScopeType.APPLICATION,
states: [
'pending',
'checking_in',
'checked_in',
'accepting_predictions',
'group_stages_underway',
'group_stages_finalized',
'underway',
'awaiting_review',
'complete'
],
resultsCount: result.length,
isOAuthAuthenticated: isAuthenticated.value,
isClientCredsAuthenticated: isClientCredsAuthenticated.value,
authType: isClientCredsAuthenticated.value
? 'Client Credentials'
: isAuthenticated.value
? 'OAuth'
: 'API Key',
results: result
});
totalTournaments.value = result.length;
hasNextPage.value = result.length >= 100;
return result;
}
});
}
async function loadMoreTournaments() {
if (apiVersion.value === 'v1') return; // v1 doesn't support pagination
currentPage.value++;
const result = await loadMoreState.execute(async () => {
const newResults = await queryAllTournaments(client.value, {
page: currentPage.value,
per_page: 100,
scopeType: tournamentScope.value
});
hasNextPage.value = newResults.length === perPage.value;
return newResults;
});
if (result) {
// Append new results to existing tournaments
tournaments.value = [...tournaments.value, ...result];
} else {
// Revert page increment on error
currentPage.value--;
}
}
async function changePerPage(newLimit) {
perPage.value = newLimit;
await testListTournaments(true);
}
async function toggleTournamentDetails(tournamentId) {
if (expandedTournamentId.value === tournamentId) {
expandedTournamentId.value = null;
tournamentDetailsState.reset();
return;
}
expandedTournamentId.value = tournamentId;
await tournamentDetailsState.execute(async () => {
if (apiVersion.value === 'v1') {
return await client.value.tournaments.get(tournamentId, {
includeParticipants: true,
includeMatches: true
});
} else {
// v2.1 get tournament
return await client.value.tournaments.get(tournamentId);
}
});
// Reset expanded state if there was an error
if (tournamentDetailsState.error.value) {
expandedTournamentId.value = null;
}
}
async function switchApiVersion() {
// Clear state when switching versions
tournamentListState.reset();
loadMoreState.reset();
tournamentDetailsState.reset();
searchQuery.value = '';
expandedTournamentId.value = null;
currentPage.value = 1;
}
function formatDate(dateString) {
if (!dateString) return '';
return new Date(dateString).toLocaleString();
}
// Watch for API version changes
watch(apiVersion, switchApiVersion);
watch(apiVersion, () => {
resetState();
});
// Check for debug mode on mount
onMounted(() => {