🔧 Refactor Challonge client and tournament logic to use reusable hooks, reducing code duplication and improving maintainability
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user