Version 1.0 #102
@@ -30,7 +30,11 @@ class GameController extends Controller
|
|||||||
$query->where("status", $request->status);
|
$query->where("status", $request->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->has("sort_direction") && in_array($request->sort_direction, ["asc", "desc"])) {
|
||||||
|
$query->orderBy("began_at", $request->sort_direction);
|
||||||
|
}else{
|
||||||
$query->orderBy("began_at", "desc");
|
$query->orderBy("began_at", "desc");
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json($query->paginate(15));
|
return response()->json($query->paginate(15));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useBiscaStore } from '@/stores/bisca'
|
import { useBiscaStore } from '@/stores/bisca'
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const app = createApp(App)
|
|||||||
|
|
||||||
//app.provide('socket', io(wsConnection))
|
//app.provide('socket', io(wsConnection))
|
||||||
app.provide('serverBaseURL', `http://${apiDomain}`)
|
app.provide('serverBaseURL', `http://${apiDomain}`)
|
||||||
app.provide('apiBaseURL', `http://${apiDomain}/api`)
|
app.provide('apiBaseURL', `http://${apiDomain}/api/v1`)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|||||||
@@ -1,71 +1,242 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { useAPIStore } from '@/stores/api'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const apiStore = useAPIStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const openGames = ref([])
|
||||||
|
const selectedMode = ref('9')
|
||||||
|
const observerTarget = ref(null);
|
||||||
|
const observer = ref(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const pageCounter = ref(1);
|
||||||
|
const showHostModal = ref(false)
|
||||||
|
const showJoinModal = ref(false)
|
||||||
|
const selectedGame = ref(null)
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const setupObserver = () => {
|
||||||
|
if (observer.value) observer.value.disconnect();
|
||||||
|
|
||||||
|
observer.value = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting && !isLoading.value && openGames.value.length > 0) {
|
||||||
|
getPendingGames();
|
||||||
|
}
|
||||||
|
}, { threshold: 0.1 });
|
||||||
|
|
||||||
|
if (observerTarget.value) {
|
||||||
|
observer.value.observe(observerTarget.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPendingGames = async (mode = selectedMode.value) => {
|
||||||
|
if (isLoading.value) return;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
|
||||||
|
apiStore.gameQueryParameters.page = pageCounter.value++;
|
||||||
|
apiStore.gameQueryParameters.filters.sort_direction = "asc"
|
||||||
|
apiStore.gameQueryParameters.filters.type = mode
|
||||||
|
apiStore.gameQueryParameters.filters.status = 'Pending'
|
||||||
|
try {
|
||||||
|
const response = await apiStore.getGames();
|
||||||
|
const newGames = response.data.data;
|
||||||
|
openGames.value = [...openGames.value, ...newGames];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickMode = async (mode) => {
|
||||||
|
if (selectedMode.value === mode) return;
|
||||||
|
openGames.value = [];
|
||||||
|
selectedMode.value = mode;
|
||||||
|
pageCounter.value = 1;
|
||||||
|
await getPendingGames();
|
||||||
|
setupObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSingle = () => {
|
||||||
|
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 = () => {
|
||||||
|
isReady.value = !isReady.value
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getPendingGames();
|
||||||
|
await nextTick();
|
||||||
|
setupObserver();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center min-h-screen bg-green-900 text-white relative">
|
<div class="min-h-screen bg-cover bg-center transition-all duration-700">
|
||||||
<h1 class="text-6xl font-bold mb-12 drop-shadow-lg">Bisca Game</h1>
|
|
||||||
|
|
||||||
<div v-if="!showModeSelection" class="flex flex-col gap-6 w-64">
|
<div class="flex flex-col justify-center items-center gap-5 pt-10 px-4">
|
||||||
<button @click="showModeSelection = true"
|
|
||||||
class="px-6 py-4 bg-emerald-600 hover:bg-emerald-500 rounded-lg text-xl font-bold shadow-lg transition-transform hover:scale-105">
|
|
||||||
Single Player
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button disabled class="px-6 py-4 bg-gray-600 rounded-lg text-xl font-bold opacity-50 cursor-not-allowed">
|
<div class="relative w-full max-w-md bg-card rounded-lg border shadow-sm overflow-hidden">
|
||||||
Multiplayer
|
<div class="relative h-14 flex items-center">
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else
|
<button @click="clickMode('9')"
|
||||||
class="bg-gray-800/90 p-8 rounded-xl border border-gray-600 shadow-2xl w-80 text-center animate-fade-in">
|
class="h-full flex items-center justify-center font-semibold transition-all duration-500 ease-in-out border-r last:border-r-0"
|
||||||
<h2 class="text-2xl font-bold mb-6 text-emerald-400">Escolhe o Modo</h2>
|
:class="{
|
||||||
|
'w-2/3 z-10 text-purple-600 dark:text-purple-400 bg-purple-500/10': selectedMode === '9',
|
||||||
<div class="flex flex-col gap-4">
|
'w-1/3 z-0 text-muted-foreground/40 bg-transparent': selectedMode !== '9'
|
||||||
<button @click="startGame(3)"
|
}">
|
||||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-bold transition-colors flex justify-between items-center">
|
|
||||||
<span>Bisca de 3</span>
|
|
||||||
<span class="text-xs bg-blue-800 px-2 py-1 rounded">Clássico</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="startGame(9)"
|
|
||||||
class="px-6 py-3 bg-purple-600 hover:bg-purple-500 rounded-lg font-bold transition-colors flex justify-between items-center">
|
|
||||||
<span>Bisca de 9</span>
|
<span>Bisca de 9</span>
|
||||||
<span class="text-xs bg-purple-800 px-2 py-1 rounded">Épico</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button @click="clickMode('3')"
|
||||||
|
class="h-full flex items-center justify-center font-semibold transition-all duration-500 ease-in-out"
|
||||||
|
:class="{
|
||||||
|
'w-2/3 z-10 text-purple-600 dark:text-purple-400 bg-purple-500/10': selectedMode === '3',
|
||||||
|
'w-1/3 z-0 text-muted-foreground/40 bg-transparent': selectedMode !== '3'
|
||||||
|
}">
|
||||||
|
<span>Bisca de 3</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button @click="showModeSelection = false" class="mt-6 text-gray-400 hover:text-white text-sm underline">
|
<div class="flex flex-col items-center gap-5 w-[100vw]">
|
||||||
Voltar atrás
|
<Card class="w-full max-w-2xl">
|
||||||
</button>
|
<CardHeader>
|
||||||
|
<CardTitle class="text-3xl font-bold text-center">
|
||||||
|
Single Player
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription class="text-center">
|
||||||
|
Go against one of our bots!
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-6">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Button @click="startSingle()" size="lg" variant="secondary"
|
||||||
|
class="hover:bg-purple-500 hover:text-slate-200">
|
||||||
|
Start Game
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card class="w-full max-w-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-3xl font-bold text-center">
|
||||||
|
MultiPlayer
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription class="text-center">
|
||||||
|
Go one on one with another player!
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-6">
|
||||||
|
<label class="text-sm font-medium">Open games (oldest first)</label>
|
||||||
|
<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="JoinGameModal(game)"
|
||||||
|
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 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">Started: {{ game.began_at }}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">host: {{
|
||||||
|
game.player1?.nickname || 'Anonymous' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref="observerTarget" class="h-10 flex items-center justify-center">
|
||||||
|
<span v-if="isLoading" class="text-xs text-purple-500 animate-pulse">Loading
|
||||||
|
more...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Button @click="hostGameModal()" size="lg" variant="secondary"
|
||||||
|
class="hover:bg-purple-500 hover:text-slate-200">
|
||||||
|
Host Game
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showHostModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<Card class="w-full max-w-sm border-purple-500 shadow-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-center">Hosting Game</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-col items-center gap-6 py-10">
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 animate-spin rounded-full border-4 border-purple-500 border-t-transparent">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="animate-pulse text-lg font-medium text-muted-foreground">Waiting for opponents...</p>
|
||||||
|
<Button variant="outline" @click="showHostModal = false">Cancel Hosting</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div v-if="showJoinModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<Card class="w-full max-w-md border-purple-500 shadow-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Join Game</CardTitle>
|
||||||
|
<CardDescription v-if="selectedGame">
|
||||||
|
Hosted by {{ selectedGame.player1?.nickname || 'Anonymous' }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="rounded-lg bg-muted p-4 text-sm">
|
||||||
|
<p><strong>Game Type:</strong> Bisca de {{ selectedMode }}</p>
|
||||||
|
<p><strong>Started at:</strong> {{ selectedGame?.began_at }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isReady"
|
||||||
|
class="flex items-center justify-center rounded-md bg-green-500/10 p-3 text-green-600 dark:text-green-400">
|
||||||
|
<span class="animate-pulse font-semibold">Waiting for the host to start...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<Button variant="destructive" class="flex-1" @click="showJoinModal = false">
|
||||||
|
Leave
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button class="flex-1 transition-all duration-300" :variant="isReady ? 'outline' : 'default'"
|
||||||
|
:class="!isReady ? 'bg-purple-600 hover:bg-purple-700' : ''" @click="toggleReady">
|
||||||
|
{{ isReady ? 'Cancel Ready' : 'Ready up' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const showModeSelection = ref(false)
|
|
||||||
|
|
||||||
const startGame = (type) => {
|
|
||||||
// Navega para a rota correta baseada no tipo (3 ou 9)
|
|
||||||
const routeName = type === 9 ? 'bisca9' : 'bisca3'
|
|
||||||
router.push({ name: routeName })
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.animate-fade-in {
|
|
||||||
animation: fadeIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user