Version 1.0 #102

Merged
FernandoJVideira merged 163 commits from develop into master 2026-01-03 14:49:00 +00:00
Showing only changes of commit 52cb893bd0 - Show all commits
+163 -86
View File
@@ -23,6 +23,11 @@ const gObserverTarget = ref(null);
const gObserver = ref(null); const gObserver = ref(null);
const gLoading = ref(false); const gLoading = ref(false);
const gPage = ref(1); const gPage = ref(1);
const openMatches = ref([])
const mObserverTarget = ref(null);
const mObserver = ref(null);
const mLoading = ref(false);
const mPage = ref(1);
const showHostModal = ref(false) const showHostModal = ref(false)
const showJoinModal = ref(false) const showJoinModal = ref(false)
const selectedGame = ref(null) const selectedGame = ref(null)
@@ -38,6 +43,21 @@ const lbObserverTarget = ref(null)
const lbObserver = ref(null) const lbObserver = ref(null)
const lbMorePages = ref(true) const lbMorePages = ref(true)
const publicStats = ref(null) const publicStats = ref(null)
const isLoggedIn = useAuthStore().isLoggedIn
const setupMatchesObserver = () => {
if (mObserver.value) mObserver.value.disconnect();
mObserver.value = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !mLoading.value && openMatches.value.length > 0) {
getPendingMatches();
}
}, { threshold: 0.1 });
if (mObserverTarget.value) {
mObserver.value.observe(mObserverTarget.value);
}
}
const setupGamesObserver = () => { const setupGamesObserver = () => {
if (gObserver.value) gObserver.value.disconnect(); if (gObserver.value) gObserver.value.disconnect();
@@ -67,14 +87,30 @@ const getPendingGames = async (mode = selectedMode.value) => {
const newGames = response.data.data; const newGames = response.data.data;
openGames.value = [...openGames.value, ...newGames]; openGames.value = [...openGames.value, ...newGames];
gPage.value++;
} catch (error) {
console.error('Failed to fetch games:', error);
} finally { } finally {
gLoading.value = false; gLoading.value = false;
} }
} }
const getPendingMatches = async (mode = selectedMode.value) => {
if (mLoading.value) return;
mLoading.value = true;
try {
const response = await axios.get(`${API_BASE_URL}/matches/open`, {
params: {
type: mode,
page: mPage.value
}
});
const newMatches = response.data.data;
openMatches.value = [...openMatches.value, ...newMatches];
} finally {
mLoading.value = false;
}
}
const fetchPublicStats = async () => { const fetchPublicStats = async () => {
try { try {
const response = await apiStore.getPublicStats() const response = await apiStore.getPublicStats()
@@ -145,81 +181,77 @@ const setupLeaderboardObserver = () => {
const clickMode = async (mode) => { const clickMode = async (mode) => {
if (selectedMode.value === mode) return; if (selectedMode.value === mode) return;
openGames.value = [];
selectedMode.value = mode; selectedMode.value = mode;
openGames.value = [];
gPage.value = 1; gPage.value = 1;
await getPendingGames(); openMatches.value = [];
mPage.value = 1;
await Promise.all([getPendingGames(), getPendingMatches()]);
setupGamesObserver(); setupGamesObserver();
setupMatchesObserver();
} }
const startSingle = () => { const startSingle = () => {
router.push({ name: 'bisca' + selectedMode.value }); router.push({ name: 'bisca' + selectedMode.value });
} }
const JoinGameModal = (game) => {
selectedGame.value = game
isReady.value = false
showJoinModal.value = true
}
const hostGameModal = () => {
showHostModal.value = true
}
const toggleReady = () => { const toggleReady = () => {
isReady.value = !isReady.value isReady.value = !isReady.value
} }
onMounted(async () => { onMounted(async () => {
await fetchPublicStats(); await fetchPublicStats();
await getPendingGames(); if (isLoggedIn) {
await nextTick(); await Promise.all([getPendingGames(), getPendingMatches()]);
setupGamesObserver(); await nextTick();
setupGamesObserver();
setupMatchesObserver();
}
}) })
const hostGame = async () => { const hostGame = async () => {
showHostModal.value = true showHostModal.value = true
try { try {
const response = await axios.post(`${API_BASE_URL}/games/host`, { const response = await axios.post(`${API_BASE_URL}/games/host`, {
type: selectedMode.value type: selectedMode.value
}) })
const gameId = response.data.id const gameId = response.data.id
console.log('Game hosted:', gameId) console.log('Game hosted:', gameId)
router.push({ router.push({
name: 'multiplayer-game', name: 'multiplayer-game',
params: { id: gameId }, params: { id: gameId },
query: { type: selectedMode.value } query: { type: selectedMode.value }
}) })
} catch (error) { } catch (error) {
console.error('Failed to host game:', error) console.error('Failed to host game:', error)
console.error('Error details:', error.response?.data) console.error('Error details:', error.response?.data)
showHostModal.value = false showHostModal.value = false
alert('Failed to host game: ' + (error.response?.data?.message || error.message)) alert('Failed to host game: ' + (error.response?.data?.message || error.message))
} }
} }
const joinGame = async (game) => { const joinGame = async (game) => {
selectedGame.value = game selectedGame.value = game
try { try {
await axios.post(`${API_BASE_URL}/games/${game.id}/join`) await axios.post(`${API_BASE_URL}/games/${game.id}/join`)
console.log('Joined game:', game.id) console.log('Joined game:', game.id)
router.push({ router.push({
name: 'multiplayer-game', name: 'multiplayer-game',
params: { id: game.id }, params: { id: game.id },
query: { type: selectedMode.value } query: { type: selectedMode.value }
}) })
} catch (error) { } catch (error) {
console.error('Failed to join game:', error) console.error('Failed to join game:', error)
console.error('Error details:', error.response?.data) console.error('Error details:', error.response?.data)
alert('Failed to join game: ' + (error.response?.data?.message || error.message)) alert('Failed to join game: ' + (error.response?.data?.message || error.message))
} }
} }
</script> </script>
@@ -227,7 +259,7 @@ const joinGame = async (game) => {
<template> <template>
<div class="min-h-screen bg-cover bg-center transition-all duration-700"> <div class="min-h-screen bg-cover bg-center transition-all duration-700">
<div class="flex flex-col justify-center items-center gap-5 pt-10 px-4"> <div class="flex flex-col justify-center items-center gap-5 pt-10 pb-10 px-4">
<Card v-if="publicStats" class="w-full max-w-2xl border-[#a855f7] bg-[#f8f0ff] shadow-sm"> <Card v-if="publicStats" class="w-full max-w-2xl border-[#a855f7] bg-[#f8f0ff] shadow-sm">
<CardContent class="p-4"> <CardContent class="p-4">
@@ -293,50 +325,95 @@ const joinGame = async (game) => {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card class="w-full max-w-2xl"> <Card class="w-full max-w-2xl flex flex-col">
<CardHeader> <CardHeader>
<CardTitle class="text-3xl font-bold text-center"> <CardTitle class="text-3xl font-bold text-center">Multi Player</CardTitle>
Multi Player <CardDescription class="text-center">Go one on one with another player!</CardDescription>
</CardTitle>
<CardDescription class="text-center">
Go one on one with another player!
</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="space-y-6">
<label class="text-sm font-medium">Open games (oldest first)</label> <CardContent class="flex-1 flex flex-col space-y-6">
<div class="rounded-lg border bg-card text-card-foreground shadow-sm"> <div class="flex flex-col flex-1">
<div v-if="openGames.length === 0" class="p-6 text-center text-sm text-muted-foreground"> <div class="flex justify-between items-end mb-2">
No open games yet. Try hosting one! <label class="text-sm font-bold uppercase tracking-wider text-muted-foreground">Open
Matches</label>
<Button v-if="isLoggedIn" @click="showHostModal()" size="sm" variant="outline"
class="border-purple-500 text-purple-500 hover:bg-purple-500 hover:text-white transition-all duration-300 font-bold">
Host
</Button>
</div> </div>
<div v-else class="max-h-[350px] overflow-y-auto custom-scrollbar p-1">
<div v-for="(game, index) in openGames" :key="game.id" @click="joinGame(game)" <div class="rounded-lg border bg-card text-card-foreground shadow-sm">
class="flex items-center justify-between p-3 mb-2 border border-transparent rounded-lg transition-all hover:bg-muted/50 hover:border-purple-500"> <div v-if="openGames.length === 0"
class="p-6 text-center text-sm text-muted-foreground">
<div class="flex items-center gap-3"> No open macthes yet. Try hosting one!
<div </div>
class="flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold bg-green-100 text-green-700"> <div v-else class="max-h-[350px] overflow-y-auto custom-scrollbar p-1">
{{ index + 1 }} <div v-for="(match, index) in openMatches" :key="match.id"
</div> class="flex items-center justify-between p-3 mb-2 border border-transparent rounded-lg cursor-pointer transition-all hover:bg-muted/50 hover:border-purple-500">
<div> <div class="flex items-center gap-3">
<div class="font-medium text-sm">Started: {{ game.began_at }}</div> <div
<div class="text-xs text-muted-foreground">host: {{ class="flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold bg-red-100 text-red-700">
game.player1?.nickname || 'Anonymous' }}</div> {{ index + 1 }}
</div>
<div>
<div class="font-medium text-sm">Match ID: {{ match.id }}</div>
<div class="text-[10px] text-muted-foreground">{{ new
Date(game.began_at).toLocaleDateString() }} {{ new
Date(game.began_at).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit' }) }}
</div>
</div>
</div> </div>
</div> </div>
</div> <div ref="mObserverTarget" class="h-10 flex items-center justify-center">
<span v-if="mLoading" class="text-xs text-purple-500 animate-pulse">Loading
<div ref="gObserverTarget" class="h-10 flex items-center justify-center"> matches...</span>
<span v-if="gLoading" class="text-xs text-purple-500 animate-pulse">Loading </div>
more...</span>
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-center">
<Button @click="hostGame()" size="lg" variant="secondary" <div class="flex flex-col flex-1">
class="hover:bg-purple-500 hover:text-slate-200"> <div class="flex justify-between items-end mb-2">
Host Game <label class="text-sm font-medium uppercase tracking-wider text-muted-foreground">Open
</Button> Games</label>
<Button v-if="isLoggedIn" @click="hostGame()" size="sm" variant="outline"
class="border-purple-500 text-purple-500 hover:bg-purple-500 hover:text-white transition-all duration-300 font-bold">
Host
</Button>
</div>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm">
<div v-if="openGames.length === 0"
class="p-6 text-center text-sm text-muted-foreground">
No open games yet. Try hosting one!
</div>
<div v-else class="max-h-[350px] overflow-y-auto custom-scrollbar p-1">
<div v-for="(game, index) in openGames" :key="game.id" @click="joinGame(game)"
class="flex items-center justify-between p-3 mb-2 border border-transparent rounded-lg cursor-pointer transition-all hover:bg-muted/50 hover:border-purple-500">
<div class="flex items-center gap-3">
<div
class="flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold bg-green-100 text-green-700">
{{ index + 1 }}
</div>
<div>
<div class="font-medium text-sm">Host: {{ game.player1?.nickname ||
'Anonymous' }}</div>
<div class="text-[10px] text-muted-foreground">{{ new
Date(game.began_at).toLocaleDateString() }} {{ new
Date(game.began_at).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit' }) }}</div>
</div>
</div>
</div>
<div ref="gObserverTarget" class="h-10 flex items-center justify-center">
<span v-if="gLoading" class="text-xs text-purple-500 animate-pulse">Loading
games...</span>
</div>
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>