Version 1.0 #102

Merged
FernandoJVideira merged 163 commits from develop into master 2026-01-03 14:49:00 +00:00
21 changed files with 400 additions and 14691 deletions
Showing only changes of commit ef0bdab0d3 - Show all commits
+2
View File
@@ -22,3 +22,5 @@
Homestead.json
Homestead.yaml
Thumbs.db
composer.lock
+5 -1
View File
@@ -30,7 +30,11 @@ class GameController extends Controller
$query->where("status", $request->status);
}
$query->orderBy("began_at", "desc");
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");
}
return response()->json($query->paginate(15));
}
+3
View File
@@ -102,12 +102,15 @@
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
-9392
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -1,9 +1,19 @@
### Get All Students
### Get All Students (Login)
# @name login
POST http://localhost:8000/api/login
content-Type: application/json
Content-Type: application/json
Accept: application/json
{
"email": "[email protected]",
"password": "123"
}
### Capture the token
@token = {{login.response.body.token}}
### Get All matches
GET http://localhost:8000/api/matches
Content-Type: application/json
Accept: application/json
Authorization: Bearer {{token}}
+3 -4
View File
@@ -9,13 +9,12 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
"lib": "@/lib"
},
"registries": {}
"iconLibrary": "lucide"
}
-5208
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,6 +21,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.562.0",
"lucide-vue-next": "^0.554.0",
"pinia": "^3.0.3",
"reka-ui": "^2.6.0",
@@ -36,7 +36,6 @@
</template>
<script setup>
import { useRouter } from 'vue-router'
import { useBiscaStore } from '@/stores/bisca'
import {
NavigationMenu,
+21
View File
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="card"
:class="
cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
props.class,
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="card-action"
:class="
cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
props.class,
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,13 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div data-slot="card-content" :class="cn('px-6', props.class)">
<slot />
</div>
</template>
@@ -0,0 +1,16 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<p
data-slot="card-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>
@@ -0,0 +1,16 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="card-footer"
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="card-header"
:class="
cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
props.class,
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,16 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<h3
data-slot="card-title"
:class="cn('leading-none font-semibold', props.class)"
>
<slot />
</h3>
</template>
+7
View File
@@ -0,0 +1,7 @@
export { default as Card } from "./Card.vue";
export { default as CardAction } from "./CardAction.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardFooter } from "./CardFooter.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";
+3
View File
@@ -39,6 +39,9 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
+1 -1
View File
@@ -1,5 +1,5 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
+1 -1
View File
@@ -15,7 +15,7 @@ const app = createApp(App)
//app.provide('socket', io(wsConnection))
app.provide('serverBaseURL', `http://${apiDomain}`)
app.provide('apiBaseURL', `http://${apiDomain}/api`)
app.provide('apiBaseURL', `http://${apiDomain}/api/v1`)
app.use(createPinia())
app.use(router)
+234 -77
View File
@@ -1,85 +1,242 @@
<template>
<div
class="flex flex-col items-center justify-center min-h-screen bg-green-900 text-white relative"
>
<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">
<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"
>
Multiplayer
</button>
</div>
<div
v-else
class="bg-gray-800/90 p-8 rounded-xl border border-gray-600 shadow-2xl w-80 text-center animate-fade-in"
>
<h2 class="text-2xl font-bold mb-6 text-emerald-400">Escolhe o Modo</h2>
<div class="flex flex-col gap-4">
<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 class="text-xs bg-purple-800 px-2 py-1 rounded">Épico</span>
</button>
</div>
<button
@click="showModeSelection = false"
class="mt-6 text-gray-400 hover:text-white text-sm underline"
>
Voltar atrás
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
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 showModeSelection = ref(false)
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 startGame = (type) => {
// Navega para a rota correta baseada no tipo (3 ou 9)
const routeName = type === 9 ? 'bisca9' : 'bisca3'
router.push({ name: routeName })
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>
<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>
<template>
<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="relative w-full max-w-md bg-card rounded-lg border shadow-sm overflow-hidden">
<div class="relative h-14 flex items-center">
<button @click="clickMode('9')"
class="h-full flex items-center justify-center font-semibold transition-all duration-500 ease-in-out border-r last:border-r-0"
:class="{
'w-2/3 z-10 text-purple-600 dark:text-purple-400 bg-purple-500/10': selectedMode === '9',
'w-1/3 z-0 text-muted-foreground/40 bg-transparent': selectedMode !== '9'
}">
<span>Bisca de 9</span>
</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 class="flex flex-col items-center gap-5 w-[100vw]">
<Card class="w-full max-w-2xl">
<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>
</template>