Version 1.0 #102

Merged
FernandoJVideira merged 163 commits from develop into master 2026-01-03 14:49:00 +00:00
11 changed files with 384 additions and 1718 deletions
Showing only changes of commit 00cb5ba966 - Show all commits
+136 -87
View File
@@ -13,17 +13,26 @@ class MatchController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$query = MatchGame::query()->with(['winner', 'player1', 'player2']); $query = MatchGame::query()->with(["winner", "player1", "player2"]);
if ($request->has('type')) { if ($request->has("type")) {
$query->where('type', $request->type); $query->where("type", $request->type);
} }
if ($request->has('status')) { if ($request->has("status")) {
$query->where('status', $request->status); $query->where("status", $request->status);
} }
$query->orderBy('began_at', 'desc'); if ($request->has("pot") && in_array($request->pot, ["asc", "desc"])) {
$query->orderBy("stake", $request->pot);
} elseif (
$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)); return response()->json($query->paginate(15));
} }
@@ -35,13 +44,20 @@ class MatchController extends Controller
$stake = $validated['stake']; $stake = $validated['stake'];
if ($user->coins_balance < $stake) { if ($user->coins_balance < $stake) {
return response()->json(['message' => 'Insufficient coin balance'], 400); return response()->json(
["message" => "Insufficient coin balance"],
400,
);
} }
$activeMatch = MatchGame::where(function ($q) use ($user) { $activeMatch = MatchGame::where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id) $q->where("player1_user_id", $user->id)->orWhere(
->orWhere('player2_user_id', $user->id); "player2_user_id",
})->whereIn('status', ['Pending', 'Playing'])->exists(); $user->id,
);
})
->whereIn("status", ["Pending", "Playing"])
->exists();
if ($activeMatch) { if ($activeMatch) {
return response()->json(['message' => 'You already have an active match.'], 400); return response()->json(['message' => 'You already have an active match.'], 400);
@@ -50,45 +66,50 @@ class MatchController extends Controller
return DB::transaction(function () use ($validated, $user, $stake) { return DB::transaction(function () use ($validated, $user, $stake) {
$GHOST_ID = 1; $GHOST_ID = 1;
$user->decrement('coins_balance', $stake); $user->decrement("coins_balance", $stake);
$stakeType = CoinTransactionType::firstOrCreate( $stakeType = CoinTransactionType::firstOrCreate(
['name' => 'Match stake'], ["name" => "Match stake"],
['type' => 'D'] // Debit ["type" => "D"], // Debit
); );
$match = MatchGame::create([ $match = MatchGame::create([
'type' => $validated['type'], "type" => $validated["type"],
'status' => 'Pending', "status" => "Pending",
'player1_user_id' => $user->id, "player1_user_id" => $user->id,
'player2_user_id' => $GHOST_ID, "player2_user_id" => $GHOST_ID,
'winner_user_id' => $GHOST_ID, "winner_user_id" => $GHOST_ID,
'loser_user_id' => $GHOST_ID, "loser_user_id" => $GHOST_ID,
'stake' => $stake, "stake" => $stake,
'began_at' => now(), "began_at" => now(),
'player1_marks' => 0, 'player2_marks' => 0, "player1_marks" => 0,
'player1_points' => 0, 'player2_points' => 0, "player2_marks" => 0,
"player1_points" => 0,
"player2_points" => 0,
]); ]);
CoinTransaction::create([ CoinTransaction::create([
'user_id' => $user->id, "user_id" => $user->id,
'match_id' => $match->id, "match_id" => $match->id,
'coin_transaction_type_id' => $stakeType->id, "coin_transaction_type_id" => $stakeType->id,
'transaction_datetime' => now(), "transaction_datetime" => now(),
'coins' => -$stake, "coins" => -$stake,
]); ]);
return response()->json([ return response()->json(
'message' => 'Match created successfully', [
'match' => $match, "message" => "Match created successfully",
'balance' => $user->coins_balance "match" => $match,
], 201); "balance" => $user->coins_balance,
],
201,
);
}); });
} }
public function show(MatchGame $match) public function show(MatchGame $match)
{ {
$match->load(['player1', 'player2', 'winner']); $match->load(["player1", "player2", "winner"]);
return response()->json($match); return response()->json($match);
} }
@@ -97,31 +118,47 @@ class MatchController extends Controller
$user = request()->user(); $user = request()->user();
if ($match->player1_user_id == $user->id) { if ($match->player1_user_id == $user->id) {
return response()->json(['message' => 'You cannot join your own match'], 400); return response()->json(
["message" => "You cannot join your own match"],
400,
);
} }
if ($match->status !== 'Pending') { if ($match->status !== "Pending") {
return response()->json(['message' => 'Match is full or started'], 400); return response()->json(
["message" => "Match is full or started"],
400,
);
} }
if ($user->coins_balance < $match->stake) { if ($user->coins_balance < $match->stake) {
return response()->json(['message' => 'Insufficient coin balance to join this match'], 400); return response()->json(
["message" => "Insufficient coin balance to join this match"],
400,
);
} }
$activeMatch = MatchGame::where(function ($q) use ($user) { $activeMatch = MatchGame::where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id) $q->where("player1_user_id", $user->id)->orWhere(
->orWhere('player2_user_id', $user->id); "player2_user_id",
})->whereIn('status', ['Pending', 'Playing'])->exists(); $user->id,
);
})
->whereIn("status", ["Pending", "Playing"])
->exists();
if ($activeMatch) { if ($activeMatch) {
return response()->json(['message' => 'You are already in another active match.'], 400); return response()->json(
["message" => "You are already in another active match."],
400,
);
} }
return DB::transaction(function () use ($match, $user) { return DB::transaction(function () use ($match, $user) {
$user->decrement('coins_balance', $match->stake); $user->decrement("coins_balance", $match->stake);
$stakeType = CoinTransactionType::firstOrCreate( $stakeType = CoinTransactionType::firstOrCreate(
['name' => 'Match stake'], ["name" => "Match stake"],
['type' => 'D'] ["type" => "D"],
); );
CoinTransaction::create([ CoinTransaction::create([
@@ -133,62 +170,63 @@ class MatchController extends Controller
]); ]);
$match->update([ $match->update([
'status' => 'Playing', "status" => "Playing",
'player2_user_id' => $user->id "player2_user_id" => $user->id,
]); ]);
return response()->json([ return response()->json([
'message' => 'Joined match successfully!', "message" => "Joined match successfully!",
'match' => $match, "match" => $match,
'balance' => $user->coins_balance "balance" => $user->coins_balance,
]); ]);
}); });
} }
public function update(Request $request, MatchGame $match) public function update(Request $request, MatchGame $match)
{ {
if ($match->status == 'Ended') { if ($match->status == "Ended") {
return response()->json(['message' => 'Match already ended'], 400); return response()->json(["message" => "Match already ended"], 400);
} }
$data = $request->validate([ $data = $request->validate([
'status' => 'in:Playing,Ended,Interrupted', "status" => "in:Playing,Ended,Interrupted",
'winner_user_id' => 'exists:users,id', "winner_user_id" => "exists:users,id",
'loser_user_id' => 'exists:users,id', "loser_user_id" => "exists:users,id",
'player1_marks' => 'integer', "player1_marks" => "integer",
'player2_marks' => 'integer', "player2_marks" => "integer",
'player1_points' => 'integer', "player1_points" => "integer",
'player2_points' => 'integer', "player2_points" => "integer",
'total_time' => 'numeric' "total_time" => "numeric",
]); ]);
return DB::transaction(function () use ($match, $data) { return DB::transaction(function () use ($match, $data) {
if (isset($data['status']) && $data['status'] === 'Ended') { if (isset($data['status']) && $data['status'] === 'Ended') {
$data['ended_at'] = now(); $data['ended_at'] = now();
} }
$match->update($data); $match->update($data);
if ($match->status === 'Ended' && $match->stake > 0 && $match->winner_user_id) { if (
$match->status === "Ended" &&
$match->stake > 0 &&
$match->winner_user_id
) {
$payoutType = CoinTransactionType::firstOrCreate( $payoutType = CoinTransactionType::firstOrCreate(
['name' => 'Match payout'], ["name" => "Match payout"],
['type' => 'C'] // Credit ["type" => "C"], // Credit
); );
$totalPrize = $match->stake * 2; $totalPrize = $match->stake * 2;
CoinTransaction::create([ CoinTransaction::create([
'user_id' => $match->winner_user_id, "user_id" => $match->winner_user_id,
'match_id' => $match->id, "match_id" => $match->id,
'coin_transaction_type_id' => $payoutType->id, "coin_transaction_type_id" => $payoutType->id,
'transaction_datetime' => now(), "transaction_datetime" => now(),
'coins' => $totalPrize, "coins" => $totalPrize,
]); ]);
$match->winner->increment("coins_balance", $totalPrize);
$match->winner->increment('coins_balance', $totalPrize);
} }
return response()->json($match); return response()->json($match);
@@ -198,34 +236,44 @@ class MatchController extends Controller
public function host(Request $request) public function host(Request $request)
{ {
$request->validate([ $request->validate([
'type' => 'required|in:3,9', "type" => "required|in:3,9",
'stake' => 'required|integer|min:0' "stake" => "required|integer|min:0",
]); ]);
$user = $request->user(); $user = $request->user();
$stake = $request->stake; $stake = $request->stake;
if ($user->coins_balance < $stake) { if ($user->coins_balance < $stake) {
return response()->json(['message' => 'Insufficient coin balance'], 400); return response()->json(
["message" => "Insufficient coin balance"],
400,
);
} }
$activeMatch = MatchGame::where(function ($q) use ($user) { $activeMatch = MatchGame::where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id) $q->where("player1_user_id", $user->id)->orWhere(
->orWhere('player2_user_id', $user->id); "player2_user_id",
})->whereIn('status', ['Pending', 'Playing'])->exists(); $user->id,
);
})
->whereIn("status", ["Pending", "Playing"])
->exists();
if ($activeMatch) { if ($activeMatch) {
return response()->json(['message' => 'You already have an active match.'], 400); return response()->json(
["message" => "You already have an active match."],
400,
);
} }
return DB::transaction(function () use ($request, $user, $stake) { return DB::transaction(function () use ($request, $user, $stake) {
$GHOST_ID = 1; $GHOST_ID = 1;
$user->decrement('coins_balance', $stake); $user->decrement("coins_balance", $stake);
$stakeType = CoinTransactionType::firstOrCreate( $stakeType = CoinTransactionType::firstOrCreate(
['name' => 'Match stake'], ["name" => "Match stake"],
['type' => 'D'] // Debit ["type" => "D"], // Debit
); );
$match = MatchGame::create([ $match = MatchGame::create([
@@ -239,14 +287,15 @@ class MatchController extends Controller
'began_at' => now(), 'began_at' => now(),
'player1_marks' => 0, 'player2_marks' => 0, 'player1_marks' => 0, 'player2_marks' => 0,
'player1_points' => 0, 'player2_points' => 0, 'player1_points' => 0, 'player2_points' => 0,
]); ]);
CoinTransaction::create([ CoinTransaction::create([
'user_id' => $user->id, "user_id" => $user->id,
'match_id' => $match->id, "match_id" => $match->id,
'coin_transaction_type_id' => $stakeType->id, "coin_transaction_type_id" => $stakeType->id,
'transaction_datetime' => now(), "transaction_datetime" => now(),
'coins' => -$stake, "coins" => -$stake,
]); ]);
return response()->json($match, 201); return response()->json($match, 201);
+149 -89
View File
@@ -18,23 +18,23 @@ class UserController extends Controller
*/ */
public function index(Request $request) public function index(Request $request)
{ {
$this->authorize('viewAny', User::class); $this->authorize("viewAny", User::class);
$query = User::query(); $query = User::query();
if ($request->has('type')) { if ($request->has("type")) {
$query->where('type', $request->type); $query->where("type", $request->type);
} }
if ($request->has('blocked')) { if ($request->has("blocked")) {
$query->where('blocked', $request->type); $query->where("blocked", $request->type);
} }
if ($request->has('search')) { if ($request->has("search")) {
$query->where(function ($q) use ($request) { $query->where(function ($q) use ($request) {
$q->where('name', 'like', '%'.$request->search.'%') $q->where("name", "like", "%" . $request->search . "%")
->orWhere('email', 'like', '%'.$request->search.'%') ->orWhere("email", "like", "%" . $request->search . "%")
->orWhere('nickname', 'like', '%'.$request->search.'%'); ->orWhere("nickname", "like", "%" . $request->search . "%");
}); });
} }
@@ -47,16 +47,16 @@ class UserController extends Controller
*/ */
public function store(Request $request) public function store(Request $request)
{ {
$this->authorize('create', User::class); $this->authorize("create", User::class);
$validated = $request->validate([ $validated = $request->validate([
'name' => 'required|string|max:255', "name" => "required|string|max:255",
'email' => 'required|email|unique:users,email', "email" => "required|email|unique:users,email",
'password' => 'required|string|min:3', "password" => "required|string|min:3",
'type' => 'required|in:A,U', "type" => "required|in:A,U",
]); ]);
$validated['password'] = Hash::make($validated['password']); $validated["password"] = Hash::make($validated["password"]);
$user = User::create($validated); $user = User::create($validated);
@@ -82,15 +82,15 @@ class UserController extends Controller
$currentUser = Auth::user(); $currentUser = Auth::user();
// Admins and owners get full profile, others get limited view // Admins and owners get full profile, others get limited view
if ($currentUser->type === 'A' || $currentUser->id === $user->id) { if ($currentUser->type === "A" || $currentUser->id === $user->id) {
return response()->json($user); return response()->json($user);
} else { } else {
return response()->json([ return response()->json([
'id' => $user->id, "id" => $user->id,
'name' => $user->name, "name" => $user->name,
'nickname' => $user->nickname, "nickname" => $user->nickname,
'photo_avatar_filename' => $user->photo_avatar_filename, "photo_avatar_filename" => $user->photo_avatar_filename,
'type' => $user->type, "type" => $user->type,
]); ]);
} }
} }
@@ -101,22 +101,22 @@ class UserController extends Controller
*/ */
public function update(Request $request, User $user) public function update(Request $request, User $user)
{ {
$this->authorize('update', $user); $this->authorize("update", $user);
$validated = $request->validate([ $validated = $request->validate([
'name' => 'sometimes|string|max:255', "name" => "sometimes|string|max:255",
'nickname' => [ "nickname" => [
'sometimes', "sometimes",
'string', "string",
'max:20', "max:20",
Rule::unique('users')->ignore($user->id), Rule::unique("users")->ignore($user->id),
], ],
'email' => [ "email" => [
'sometimes', "sometimes",
'email', "email",
Rule::unique('users')->ignore($user->id), Rule::unique("users")->ignore($user->id),
], ],
'password' => 'sometimes|string|min:3', "password" => "sometimes|string|min:3",
]); ]);
// Lógica de Upload de Foto (Exemplo Básico) // Lógica de Upload de Foto (Exemplo Básico)
@@ -130,26 +130,28 @@ class UserController extends Controller
return response()->json($user); return response()->json($user);
} }
public function updatePassword(Request $request, User $user) public function updatePassword(Request $request, User $user)
{ {
$this->authorize('update', $user); $this->authorize("update", $user);
$validated = $request->validate([ $validated = $request->validate([
'current_password' => 'required|string', "current_password" => "required|string",
'new_password' => 'required|string|min:3|confirmed', "new_password" => "required|string|min:3|confirmed",
]); ]);
// Verificar se a password atual está correta // Verificar se a password atual está correta
if (!Hash::check($validated['current_password'], $user->password)) { if (!Hash::check($validated["current_password"], $user->password)) {
return response()->json(['message' => 'Current password is incorrect'], 403); return response()->json(
["message" => "Current password is incorrect"],
403,
);
} }
// Atualizar para a nova password // Atualizar para a nova password
$user->password = Hash::make($validated['new_password']); $user->password = Hash::make($validated["new_password"]);
$user->save(); $user->save();
return response()->json(['message' => 'Password updated successfully']); return response()->json(["message" => "Password updated successfully"]);
} }
/** /**
@@ -158,24 +160,27 @@ class UserController extends Controller
*/ */
public function destroy(User $user) public function destroy(User $user)
{ {
$this->authorize('delete', $user); $this->authorize("delete", $user);
// Check if user has transactions or games - if so, soft delete // Check if user has transactions or games - if so, soft delete
$hasTransactions = $user->transactions()->exists(); $hasTransactions = $user->transactions()->exists();
$hasGames = Game::where(function ($q) use ($user) { $hasGames = Game::where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id) $q->where("player1_user_id", $user->id)->orWhere(
->orWhere('player2_user_id', $user->id); "player2_user_id",
$user->id,
);
})->exists(); })->exists();
if ($hasTransactions || $hasGames) { if ($hasTransactions || $hasGames) {
$user->delete(); // Soft delete $user->delete(); // Soft delete
$message = 'User account deactivated (soft-deleted due to transaction/game history)'; $message =
"User account deactivated (soft-deleted due to transaction/game history)";
} else { } else {
$user->forceDelete(); // Hard delete $user->forceDelete(); // Hard delete
$message = 'User permanently deleted'; $message = "User permanently deleted";
} }
return response()->json(['message' => $message]); return response()->json(["message" => $message]);
} }
/** /**
@@ -184,37 +189,61 @@ class UserController extends Controller
*/ */
public function getGames(Request $request, User $user) public function getGames(Request $request, User $user)
{ {
$this->authorize('view', $user); $this->authorize("view", $user);
// Define a query base para GAMES // Define a query base para GAMES
$query = \App\Models\Game::query() $query = \App\Models\Game::query()
->where(function ($q) use ($user) { ->where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id) $q->where("player1_user_id", $user->id)->orWhere(
->orWhere('player2_user_id', $user->id); "player2_user_id",
$user->id,
);
}) })
->with(['winner', 'player1', 'player2']); ->with(["winner", "player1", "player2"]);
if ($request->has('type')) { if ($request->has("type")) {
$query->where('type', $request->type); $query->where("type", $request->type);
} }
if ($request->has('status')) { if ($request->has("status")) {
$query->where('status', $request->status); $query->where("status", $request->status);
} }
$sortDirection = $request->get('sort_direction', 'desc'); // Date filtering
$query->orderBy('began_at', $sortDirection); if ($request->has("date_from")) {
$query->whereDate("began_at", ">=", $request->date_from);
}
if ($request->has("date_to")) {
$query->whereDate("began_at", "<=", $request->date_to);
}
// Flexible sorting
$sortColumn = $request->get("sort_by", "began_at");
$sortDirection = $request->get("sort_direction", "desc");
// Whitelist sortable columns for security
$allowedColumns = [
"began_at",
"ended_at",
"total_time",
"total_points",
];
if (in_array($sortColumn, $allowedColumns)) {
$query->orderBy($sortColumn, $sortDirection);
} else {
$query->orderBy("began_at", $sortDirection);
}
$games = $query->paginate(10); $games = $query->paginate(10);
$games->getCollection()->transform(function ($game) use ($user) { $games->getCollection()->transform(function ($game) use ($user) {
if ($game->winner_user_id == $user->id) { if ($game->winner_user_id == $user->id) {
$game->outcome = 'win'; $game->outcome = "win";
} elseif ($game->is_draw) { } elseif ($game->is_draw) {
$game->outcome = 'draw'; $game->outcome = "draw";
} else { } else {
$game->outcome = 'loss'; $game->outcome = "loss";
} }
return $game; return $game;
}); });
@@ -228,35 +257,58 @@ class UserController extends Controller
*/ */
public function getMatches(Request $request, User $user) public function getMatches(Request $request, User $user)
{ {
$this->authorize('view', $user); $this->authorize("view", $user);
$query = MatchGame::query() $query = MatchGame::query()
->where(function ($q) use ($user) { ->where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id) $q->where("player1_user_id", $user->id)->orWhere(
->orWhere('player2_user_id', $user->id); "player2_user_id",
$user->id,
);
}) })
->with(['winner', 'player1', 'player2']); ->with(["winner", "player1", "player2"]);
if ($request->has('type')) { if ($request->has("type")) {
$query->where('type', $request->type); $query->where("type", $request->type);
} }
if ($request->has('status')) { if ($request->has("status")) {
$query->where('status', $request->status); $query->where("status", $request->status);
} }
$sortDirection = $request->get('sort_direction', 'desc'); // Date filtering
$query->orderBy('began_at', $sortDirection); if ($request->has("date_from")) {
$query->whereDate("began_at", ">=", $request->date_from);
}
if ($request->has("date_to")) {
$query->whereDate("began_at", "<=", $request->date_to);
}
// Flexible sorting
$sortColumn = $request->get("sort_by", "began_at");
$sortDirection = $request->get("sort_direction", "desc");
// Whitelist sortable columns for security
$allowedColumns = ["began_at", "ended_at", "stake", "total_time"];
if (in_array($sortColumn, $allowedColumns)) {
$query->orderBy($sortColumn, $sortDirection);
} else {
$query->orderBy("began_at", $sortDirection);
}
$matches = $query->paginate(10); $matches = $query->paginate(10);
$matches->getCollection()->transform(function ($match) use ($user) { $matches->getCollection()->transform(function ($match) use ($user) {
if ($match->winner_user_id == $user->id) { if ($match->winner_user_id == $user->id) {
$match->outcome = 'win'; $match->outcome = "win";
} elseif ($match->winner_user_id && $match->winner_user_id != $user->id) { } elseif (
$match->outcome = 'loss'; $match->winner_user_id &&
$match->winner_user_id != $user->id
) {
$match->outcome = "loss";
} else { } else {
$match->outcome = 'interrupted'; // Matches geralmente não empatam (alguém chega a 4 marcas) $match->outcome = "interrupted"; // Matches geralmente não empatam (alguém chega a 4 marcas)
} }
return $match; return $match;
}); });
@@ -266,7 +318,7 @@ class UserController extends Controller
public function block(User $user) public function block(User $user)
{ {
$this->authorize('block', $user); $this->authorize("block", $user);
$user->blocked = true; $user->blocked = true;
$user->save(); $user->save();
@@ -274,42 +326,50 @@ class UserController extends Controller
// Revoke all active tokens for immediate effect // Revoke all active tokens for immediate effect
$user->tokens()->delete(); $user->tokens()->delete();
return response()->json(['message' => 'User blocked successfully']); return response()->json(["message" => "User blocked successfully"]);
} }
public function unblock(User $user) public function unblock(User $user)
{ {
$this->authorize('unblock', $user); $this->authorize("unblock", $user);
$user->blocked = false; $user->blocked = false;
$user->save(); $user->save();
return response()->json(['message' => 'User unblocked successfully']); return response()->json(["message" => "User unblocked successfully"]);
} }
public function getTransactions(Request $request, User $user) public function getTransactions(Request $request, User $user)
{ {
if ($request->user()->id !== $user->id && $request->user()->type !== 'A') { if (
return response()->json(['message' => 'Forbidden'], 403); $request->user()->id !== $user->id &&
$request->user()->type !== "A"
) {
return response()->json(["message" => "Forbidden"], 403);
} }
$query = $user->transactions()->with('type'); $query = $user->transactions()->with("type");
if ($request->has('type')) { if ($request->has("type")) {
$query->whereHas('type', function ($q) use ($request) { $query->whereHas("type", function ($q) use ($request) {
$q->where('type', $request->type); $q->where("type", $request->type);
}); });
} }
if ($request->has('date_from')) { if ($request->has("date_from")) {
$query->whereDate('transaction_datetime', '>=', $request->date_from); $query->whereDate(
"transaction_datetime",
">=",
$request->date_from,
);
} }
if ($request->has('date_to')) { if ($request->has("date_to")) {
$query->whereDate('transaction_datetime', '<=', $request->date_to); $query->whereDate("transaction_datetime", "<=", $request->date_to);
} }
$transactions = $query->orderBy('transaction_datetime', 'desc') $transactions = $query
->orderBy("transaction_datetime", "desc")
->paginate(10); ->paginate(10);
return response()->json($transactions); return response()->json($transactions);
+8 -21
View File
@@ -2,20 +2,6 @@
<div> <div>
<NavigationMenu> <NavigationMenu>
<NavigationMenuList class="justify-around gap-20"> <NavigationMenuList class="justify-around gap-20">
<NavigationMenuItem>
<NavigationMenuTrigger>Testing</NavigationMenuTrigger>
<NavigationMenuContent>
<li>
<NavigationMenuLink as-child>
<RouterLink to="/testing/laravel">Laravel</RouterLink>
</NavigationMenuLink>
<NavigationMenuLink as-child>
<RouterLink to="/testing/websockets">Web Sockets</RouterLink>
</NavigationMenuLink>
</li>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem v-if="!userLoggedIn"> <NavigationMenuItem v-if="!userLoggedIn">
<NavigationMenuLink> <NavigationMenuLink>
<RouterLink to="/login">Login</RouterLink> <RouterLink to="/login">Login</RouterLink>
@@ -55,10 +41,9 @@ import {
NavigationMenuList, NavigationMenuList,
NavigationMenuTrigger, NavigationMenuTrigger,
} from '@/components/ui/navigation-menu' } from '@/components/ui/navigation-menu'
import router from '@/router'; import router from '@/router'
import { watch } from 'vue'; import { watch } from 'vue'
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user'
const emits = defineEmits(['logout']) const emits = defineEmits(['logout'])
const { userLoggedIn } = defineProps(['userLoggedIn']) const { userLoggedIn } = defineProps(['userLoggedIn'])
@@ -68,7 +53,7 @@ const biscaStore = useBiscaStore()
const logoutClickHandler = () => { const logoutClickHandler = () => {
if (biscaStore.isGameRunning) { if (biscaStore.isGameRunning) {
const confirmLogout = window.confirm( const confirmLogout = window.confirm(
'If you leave now this game will count as "FORFEIT". Are you sure you want to logout?' 'If you leave now this game will count as "FORFEIT". Are you sure you want to logout?',
) )
if (!confirmLogout) return if (!confirmLogout) return
@@ -85,11 +70,13 @@ const logoutClickHandler = () => {
const userStore = useUserStore() const userStore = useUserStore()
watch(() => userLoggedIn, async (isLoggedIn) => { watch(
() => userLoggedIn,
async (isLoggedIn) => {
if (isLoggedIn) { if (isLoggedIn) {
await userStore.fetchCoins() await userStore.fetchCoins()
} else { } else {
userStore.coins = 0 userStore.coins = 0
} }
}, { immediate: true }) }, { immediate: true })
</script> </script>
-342
View File
@@ -1,342 +0,0 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900 p-8">
<div class="max-w-6xl mx-auto">
<!-- Title -->
<h1 class="text-white text-3xl font-bold mb-8 text-center">
All Animations Test Page
</h1>
<!-- Control Panel -->
<div class="bg-gray-900/80 rounded-lg p-6 mb-8">
<h2 class="text-white text-xl font-semibold mb-4">Controls</h2>
<!-- Score Animation Tests -->
<div class="mb-6">
<h3 class="text-emerald-400 font-semibold mb-3">Score Count-Up Animation</h3>
<div class="flex flex-wrap gap-3">
<button
@click="increasePlayerScore"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium transition-colors"
>
+10 Player Score
</button>
<button
@click="increaseOpponentScore"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium transition-colors"
>
+10 Opponent Score
</button>
<button
@click="resetScores"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
>
Reset Scores
</button>
</div>
</div>
<!-- Turn Transition Tests -->
<div class="mb-6">
<h3 class="text-blue-400 font-semibold mb-3">Turn Transition Animation</h3>
<div class="flex flex-wrap gap-3">
<button
@click="toggleTurn"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
>
Switch Turn
</button>
<button
@click="currentTurn = 'player'"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-colors"
>
Your Turn
</button>
<button
@click="currentTurn = 'opponent'"
class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-medium transition-colors"
>
Bot's Turn
</button>
</div>
</div>
<!-- Trump Reveal Tests -->
<div class="mb-6">
<h3 class="text-amber-400 font-semibold mb-3">Trump Reveal Animation</h3>
<div class="flex flex-wrap gap-3">
<button
@click="triggerTrumpReveal"
class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium transition-colors"
>
Reveal Trump
</button>
</div>
</div>
<!-- Game Over Tests -->
<div class="mb-6">
<h3 class="text-purple-400 font-semibold mb-3">Game Over Screen</h3>
<div class="flex flex-wrap gap-3">
<button
@click="showGameOverPlayerWin"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-colors"
>
Player Wins
</button>
<button
@click="showGameOverOpponentWin"
class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-medium transition-colors"
>
Opponent Wins
</button>
<button
@click="showGameOverDraw"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
>
Draw
</button>
</div>
</div>
<!-- Full Sequence -->
<div>
<h3 class="text-pink-400 font-semibold mb-3">Complete Sequence</h3>
<div class="flex flex-wrap gap-3">
<button
@click="runFullSequence"
:disabled="isRunningSequence"
class="px-4 py-2 bg-pink-600 hover:bg-pink-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
>
Run Full Animation Sequence
</button>
</div>
</div>
</div>
<!-- Game Board Preview -->
<div class="grid grid-rows-[auto_1fr] gap-8">
<!-- Score Display -->
<div class="flex justify-center">
<ScoreDisplay
:player-score="playerScore"
:opponent-score="opponentScore"
:current-turn="currentTurn"
:round-number="1"
/>
</div>
<!-- Deck Area (with trump) -->
<div class="flex justify-center">
<div class="bg-gray-800/30 rounded-lg p-6">
<h3 class="text-white text-sm font-semibold mb-4 text-center">Trump Card</h3>
<DeckArea
:trump-card="trumpCard"
:cards-remaining="cardsRemaining"
:is-empty="false"
:reveal-trump="revealTrump"
/>
</div>
</div>
</div>
<!-- Instructions -->
<div class="bg-gray-900/80 rounded-lg p-6 mt-8 text-gray-300">
<h3 class="text-white font-semibold mb-2">Instructions:</h3>
<ul class="list-disc list-inside space-y-1 text-sm">
<li>
<strong>Score Count-Up:</strong> Click "+10" buttons to see scores animate smoothly
from old to new value
</li>
<li>
<strong>Turn Transition:</strong> Switch turns to see the indicator pulse and glow with
color change
</li>
<li>
<strong>Trump Reveal:</strong> Triggers a 3-second pulse/glow animation on the trump
card with label
</li>
<li>
<strong>Game Over:</strong> Shows victory/defeat modal with score count-up and confetti
(on win)
</li>
<li>
<strong>Full Sequence:</strong> Runs all animations in order: trump reveal → turn
changes → score updates → game over
</li>
</ul>
</div>
</div>
<!-- Game Over Modal -->
<GameOver
:is-visible="gameOverVisible"
:winner="gameOverWinner"
:player-score="playerScore"
:opponent-score="opponentScore"
:stats="gameOverStats"
@close="gameOverVisible = false"
@play-again="handlePlayAgain"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ScoreDisplay from '@/components/game/ScoreDisplay.vue'
import DeckArea from '@/components/game/DeckArea.vue'
import GameOver from '@/components/game/GameOver.vue'
// Game state
const playerScore = ref(23)
const opponentScore = ref(15)
const currentTurn = ref('player')
const cardsRemaining = ref(30)
const revealTrump = ref(false)
const isRunningSequence = ref(false)
// Trump card
const trumpCard = ref({ suit: 'o', rank: 7 })
// Game Over state
const gameOverVisible = ref(false)
const gameOverWinner = ref('player')
const gameOverStats = ref({
playerTricks: 8,
opponentTricks: 6,
})
// Score manipulation
const increasePlayerScore = () => {
playerScore.value += 10
}
const increaseOpponentScore = () => {
opponentScore.value += 10
}
const resetScores = () => {
playerScore.value = 0
opponentScore.value = 0
}
// Turn control
const toggleTurn = () => {
currentTurn.value = currentTurn.value === 'player' ? 'opponent' : 'player'
}
// Trump reveal
const triggerTrumpReveal = () => {
revealTrump.value = true
setTimeout(() => {
revealTrump.value = false
}, 100)
}
// Game Over screens
const showGameOverPlayerWin = () => {
playerScore.value = 67
opponentScore.value = 53
gameOverWinner.value = 'player'
gameOverStats.value = {
playerTricks: 11,
opponentTricks: 9,
}
gameOverVisible.value = true
}
const showGameOverOpponentWin = () => {
playerScore.value = 45
opponentScore.value = 75
gameOverWinner.value = 'opponent'
gameOverStats.value = {
playerTricks: 7,
opponentTricks: 13,
}
gameOverVisible.value = true
}
const showGameOverDraw = () => {
playerScore.value = 60
opponentScore.value = 60
gameOverWinner.value = 'draw'
gameOverStats.value = {
playerTricks: 10,
opponentTricks: 10,
}
gameOverVisible.value = true
}
const handlePlayAgain = () => {
gameOverVisible.value = false
resetScores()
currentTurn.value = 'player'
cardsRemaining.value = 40
}
// Full animation sequence
const runFullSequence = async () => {
if (isRunningSequence.value) return
isRunningSequence.value = true
// Reset everything
resetScores()
currentTurn.value = 'player'
gameOverVisible.value = false
await new Promise((resolve) => setTimeout(resolve, 500))
// 1. Trump Reveal
console.log('1. Trump Reveal')
triggerTrumpReveal()
await new Promise((resolve) => setTimeout(resolve, 3500))
// 2. First turn
console.log('2. Player turn')
currentTurn.value = 'player'
await new Promise((resolve) => setTimeout(resolve, 1500))
// 3. Player scores
console.log('3. Player scores')
playerScore.value = 11
await new Promise((resolve) => setTimeout(resolve, 2000))
// 4. Switch turn
console.log('4. Switch to opponent turn')
currentTurn.value = 'opponent'
await new Promise((resolve) => setTimeout(resolve, 1500))
// 5. Opponent scores
console.log('5. Opponent scores')
opponentScore.value = 10
await new Promise((resolve) => setTimeout(resolve, 2000))
// 6. Back to player
console.log('6. Back to player')
currentTurn.value = 'player'
await new Promise((resolve) => setTimeout(resolve, 1000))
// 7. More scoring
console.log('7. More scoring')
playerScore.value = 23
await new Promise((resolve) => setTimeout(resolve, 1500))
opponentScore.value = 19
await new Promise((resolve) => setTimeout(resolve, 1500))
playerScore.value = 34
await new Promise((resolve) => setTimeout(resolve, 1500))
// 8. Final scores and game over
console.log('8. Game over')
playerScore.value = 67
opponentScore.value = 53
await new Promise((resolve) => setTimeout(resolve, 2000))
// Show game over
gameOverWinner.value = 'player'
gameOverStats.value = {
playerTricks: 11,
opponentTricks: 9,
}
gameOverVisible.value = true
isRunningSequence.value = false
}
</script>
-252
View File
@@ -1,252 +0,0 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900 p-8">
<div class="max-w-4xl mx-auto">
<!-- Title -->
<h1 class="text-white text-3xl font-bold mb-8 text-center">Animation Test Page</h1>
<!-- Control Panel -->
<div class="bg-gray-900/80 rounded-lg p-6 mb-8">
<h2 class="text-white text-xl font-semibold mb-4">Controls</h2>
<div class="flex flex-wrap gap-3">
<button
@click="playPlayerCard"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium transition-colors"
>
Play Player Card
</button>
<button
@click="playOpponentCard"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium transition-colors"
>
Play Opponent Card
</button>
<button
@click="playBothCards"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
>
Play Both Cards
</button>
<button
@click="clearCards"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
>
Clear Cards
</button>
<button
@click="setPlayerWinner"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-colors"
>
Player Wins
</button>
<button
@click="setOpponentWinner"
class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-medium transition-colors"
>
Opponent Wins
</button>
<button
@click="fullSequence"
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium transition-colors"
>
Full Trick Sequence
</button>
<button
@click="collectTrickPlayerWins"
:disabled="!playerCard || !opponentCard"
class="px-4 py-2 bg-cyan-600 hover:bg-cyan-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
>
Collect Trick (Player Wins)
</button>
<button
@click="collectTrickOpponentWins"
:disabled="!playerCard || !opponentCard"
class="px-4 py-2 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
>
Collect Trick (Opponent Wins)
</button>
</div>
<!-- Current State Display -->
<div class="mt-4 text-gray-300 text-sm">
<p>
<strong>Player Card:</strong>
{{ playerCard ? `${playerCard.suit}${playerCard.rank}` : 'None' }}
</p>
<p>
<strong>Opponent Card:</strong>
{{ opponentCard ? `${opponentCard.suit}${opponentCard.rank}` : 'None' }}
</p>
<p><strong>Winner:</strong> {{ winner || 'None' }}</p>
<p><strong>First Player:</strong> {{ firstPlayer || 'None' }}</p>
</div>
</div>
<!-- PlayArea Component -->
<div class="bg-gray-800/50 rounded-lg p-8">
<h2 class="text-white text-xl font-semibold mb-4 text-center">Play Area</h2>
<PlayArea
:player-card="playerCard"
:opponent-card="opponentCard"
:winner="winner"
:first-player="firstPlayer"
/>
</div>
<!-- Instructions -->
<div class="bg-gray-900/80 rounded-lg p-6 mt-8 text-gray-300">
<h3 class="text-white font-semibold mb-2">Instructions:</h3>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><strong>Play Player Card:</strong> Animates a card from bottom (player's hand)</li>
<li><strong>Play Opponent Card:</strong> Animates a card from top (opponent's hand)</li>
<li><strong>Play Both Cards:</strong> Both cards animate in simultaneously</li>
<li><strong>Clear Cards:</strong> Triggers exit animations (cards slide out)</li>
<li>
<strong>Winner Buttons:</strong> Add glowing ring effect to winning card (play cards
first)
</li>
<li>
<strong>Full Trick Sequence:</strong> Plays both cards shows winner clears (2 second
delay)
</li>
<li>
<strong>Collect Trick (Player Wins):</strong> Sets player as winner, waits 1.5s, then
cards slide DOWN toward player area
</li>
<li>
<strong>Collect Trick (Opponent Wins):</strong> Sets opponent as winner, waits 1.5s,
then cards slide UP toward opponent area
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import PlayArea from '@/components/game/PlayArea.vue'
// Reactive state
const playerCard = ref(null)
const opponentCard = ref(null)
const winner = ref(null)
const firstPlayer = ref(null)
// Sample cards to test with
const sampleCards = [
{ suit: 'c', rank: 1 },
{ suit: 'c', rank: 7 },
{ suit: 'c', rank: 13 },
{ suit: 'e', rank: 11 },
{ suit: 'e', rank: 12 },
{ suit: 'o', rank: 2 },
{ suit: 'o', rank: 3 },
{ suit: 'p', rank: 4 },
{ suit: 'p', rank: 7 },
]
// Get random card
const getRandomCard = () => {
return sampleCards[Math.floor(Math.random() * sampleCards.length)]
}
// Control functions
const playPlayerCard = () => {
if (!playerCard.value && !opponentCard.value) {
firstPlayer.value = 'player'
}
playerCard.value = getRandomCard()
winner.value = null
}
const playOpponentCard = () => {
if (!playerCard.value && !opponentCard.value) {
firstPlayer.value = 'opponent'
}
opponentCard.value = getRandomCard()
winner.value = null
}
const playBothCards = () => {
firstPlayer.value = 'player' // Player plays first in this scenario
playerCard.value = getRandomCard()
opponentCard.value = getRandomCard()
winner.value = null
}
const clearCards = () => {
playerCard.value = null
opponentCard.value = null
winner.value = null
firstPlayer.value = null
}
const setPlayerWinner = () => {
if (playerCard.value) {
winner.value = 'player'
}
}
const setOpponentWinner = () => {
if (opponentCard.value) {
winner.value = 'opponent'
}
}
const fullSequence = async () => {
// Clear first
clearCards()
await new Promise((resolve) => setTimeout(resolve, 500))
// Play both cards
playBothCards()
await new Promise((resolve) => setTimeout(resolve, 1000))
// Show winner (randomly)
winner.value = Math.random() > 0.5 ? 'player' : 'opponent'
await new Promise((resolve) => setTimeout(resolve, 2000))
// Clear cards
clearCards()
}
// Collect trick with player winning
const collectTrickPlayerWins = async () => {
if (!playerCard.value || !opponentCard.value) return
// Set player as winner
winner.value = 'player'
// Wait 1.5 seconds to show winner glow
await new Promise((resolve) => setTimeout(resolve, 1500))
// Clear cards (triggers exit animation toward player)
playerCard.value = null
opponentCard.value = null
// Reset after animation completes
await new Promise((resolve) => setTimeout(resolve, 600))
winner.value = null
firstPlayer.value = null
}
// Collect trick with opponent winning
const collectTrickOpponentWins = async () => {
if (!playerCard.value || !opponentCard.value) return
// Set opponent as winner
winner.value = 'opponent'
// Wait 1.5 seconds to show winner glow
await new Promise((resolve) => setTimeout(resolve, 1500))
// Clear cards (triggers exit animation toward opponent)
playerCard.value = null
opponentCard.value = null
// Reset after animation completes
await new Promise((resolve) => setTimeout(resolve, 600))
winner.value = null
firstPlayer.value = null
}
</script>
-275
View File
@@ -1,275 +0,0 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900 p-8">
<div class="max-w-6xl mx-auto">
<!-- Title -->
<h1 class="text-white text-3xl font-bold mb-8 text-center">Card Dealing Test Page</h1>
<!-- Control Panel -->
<div class="bg-gray-900/80 rounded-lg p-6 mb-8">
<h2 class="text-white text-xl font-semibold mb-4">Controls</h2>
<div class="flex flex-wrap gap-3 mb-4">
<button
@click="startDealing"
:disabled="isDealing"
class="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
>
Start Dealing
</button>
<button
@click="dealSingleCard"
:disabled="isDealing || cardsRemaining === 0"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors"
>
Deal One Card
</button>
<button
@click="reset"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium transition-colors"
>
Reset
</button>
</div>
<!-- Current State Display -->
<div class="grid grid-cols-3 gap-4 text-gray-300 text-sm">
<div>
<p><strong>Player Hand:</strong> {{ playerHand.length }} cards</p>
<p><strong>Opponent Hand:</strong> {{ opponentHand.length }} cards</p>
</div>
<div>
<p><strong>Cards Remaining:</strong> {{ cardsRemaining }}</p>
<p><strong>Dealing:</strong> {{ isDealing ? 'Yes' : 'No' }}</p>
</div>
<div>
<p><strong>Next Recipient:</strong> {{ nextRecipient }}</p>
</div>
</div>
</div>
<!-- Game Board Layout -->
<div class="grid grid-rows-[auto_1fr_auto] gap-8">
<!-- Opponent Hand (Top) -->
<div class="flex justify-center">
<div class="bg-gray-800/30 rounded-lg p-4">
<h3 class="text-white text-sm font-semibold mb-2 text-center">Opponent Hand</h3>
<PlayerHand
ref="opponentHandRef"
:cards="opponentHand"
:face-down="true"
:max-cards="3"
:playable-cards="[]"
/>
</div>
</div>
<!-- Center Area (Deck) -->
<div class="flex items-center justify-center">
<div class="bg-gray-800/30 rounded-lg p-6">
<h3 class="text-white text-sm font-semibold mb-4 text-center">Deck</h3>
<DeckArea
ref="deckRef"
:trump-card="trumpCard"
:cards-remaining="cardsRemaining"
:is-empty="cardsRemaining === 0"
/>
</div>
</div>
<!-- Player Hand (Bottom) -->
<div class="flex justify-center">
<div class="bg-gray-800/30 rounded-lg p-4">
<h3 class="text-white text-sm font-semibold mb-2 text-center">Your Hand</h3>
<PlayerHand
ref="playerHandRef"
:cards="playerHand"
:face-down="false"
:max-cards="3"
:playable-cards="[]"
/>
</div>
</div>
</div>
<!-- Instructions -->
<div class="bg-gray-900/80 rounded-lg p-6 mt-8 text-gray-300">
<h3 class="text-white font-semibold mb-2">Instructions:</h3>
<ul class="list-disc list-inside space-y-1 text-sm">
<li><strong>Start Dealing:</strong> Automatically deals 6 cards (3 to each player) with 200ms delay</li>
<li><strong>Deal One Card:</strong> Manually deal a single card to the next recipient</li>
<li><strong>Reset:</strong> Clear all hands and reset the deck to 40 cards</li>
<li>Watch the FlyingCard animation as cards move from deck to hands</li>
<li>Notice the deck counter decreasing and the pulse effect</li>
<li>Cards appear in hands with a fade-in animation</li>
</ul>
</div>
</div>
<!-- Flying Cards Container -->
<FlyingCard
v-for="flyingCard in flyingCards"
:key="flyingCard.id"
:card="flyingCard.card"
:start-position="flyingCard.startPosition"
:end-position="flyingCard.endPosition"
:duration="500"
:face-down="flyingCard.faceDown"
:delay="flyingCard.delay"
@complete="onFlyingCardComplete(flyingCard)"
/>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import PlayerHand from '@/components/game/PlayerHand.vue'
import DeckArea from '@/components/game/DeckArea.vue'
import FlyingCard from '@/components/game/FlyingCard.vue'
// Refs for components
const playerHandRef = ref(null)
const opponentHandRef = ref(null)
const deckRef = ref(null)
// Game state
const playerHand = ref([])
const opponentHand = ref([])
const cardsRemaining = ref(40)
const isDealing = ref(false)
const flyingCards = ref([])
let flyingCardIdCounter = 0
// Trump card (fixed for testing)
const trumpCard = ref({ suit: 'o', rank: 7 })
// Sample deck
const sampleCards = [
{ suit: 'c', rank: 1 },
{ suit: 'c', rank: 2 },
{ suit: 'c', rank: 3 },
{ suit: 'c', rank: 4 },
{ suit: 'c', rank: 5 },
{ suit: 'c', rank: 6 },
{ suit: 'c', rank: 7 },
{ suit: 'c', rank: 11 },
{ suit: 'c', rank: 12 },
{ suit: 'c', rank: 13 },
{ suit: 'e', rank: 1 },
{ suit: 'e', rank: 2 },
{ suit: 'e', rank: 3 },
{ suit: 'e', rank: 4 },
{ suit: 'e', rank: 5 },
{ suit: 'e', rank: 6 },
{ suit: 'e', rank: 7 },
{ suit: 'e', rank: 11 },
{ suit: 'e', rank: 12 },
{ suit: 'e', rank: 13 },
{ suit: 'o', rank: 1 },
{ suit: 'o', rank: 2 },
{ suit: 'o', rank: 3 },
{ suit: 'o', rank: 4 },
{ suit: 'o', rank: 5 },
{ suit: 'o', rank: 6 },
{ suit: 'p', rank: 1 },
{ suit: 'p', rank: 2 },
{ suit: 'p', rank: 3 },
{ suit: 'p', rank: 4 },
{ suit: 'p', rank: 5 },
{ suit: 'p', rank: 6 },
{ suit: 'p', rank: 7 },
{ suit: 'p', rank: 11 },
{ suit: 'p', rank: 12 },
{ suit: 'p', rank: 13 },
]
// Computed
const nextRecipient = computed(() => {
const totalDealt = playerHand.value.length + opponentHand.value.length
return totalDealt % 2 === 0 ? 'Player' : 'Opponent'
})
// Get random card
const getRandomCard = () => {
return sampleCards[Math.floor(Math.random() * sampleCards.length)]
}
// Get position of an element
const getElementPosition = (el) => {
if (!el) return { x: 0, y: 0 }
const rect = el.$el?.getBoundingClientRect() || el.getBoundingClientRect()
return {
x: rect.left + rect.width / 2 - 64, // Center, minus half card width
y: rect.top + rect.height / 2 - 89, // Center, minus half card height
}
}
// Deal a single card
const dealSingleCard = async () => {
if (cardsRemaining.value === 0) return
await nextTick()
// Determine recipient
const totalDealt = playerHand.value.length + opponentHand.value.length
const isPlayer = totalDealt % 2 === 0
// Get positions
const startPos = getElementPosition(deckRef.value)
const endPos = getElementPosition(isPlayer ? playerHandRef.value : opponentHandRef.value)
// Create flying card
const card = getRandomCard()
const flyingCard = {
id: flyingCardIdCounter++,
card,
startPosition: startPos,
endPosition: endPos,
faceDown: !isPlayer, // Face down for opponent
delay: 0,
recipient: isPlayer ? 'player' : 'opponent',
}
flyingCards.value.push(flyingCard)
cardsRemaining.value--
}
// Flying card animation complete
const onFlyingCardComplete = (flyingCard) => {
// Add card to appropriate hand
if (flyingCard.recipient === 'player') {
playerHand.value.push(flyingCard.card)
} else {
opponentHand.value.push(flyingCard.card)
}
// Remove flying card
flyingCards.value = flyingCards.value.filter((fc) => fc.id !== flyingCard.id)
}
// Start automatic dealing sequence
const startDealing = async () => {
if (isDealing.value) return
isDealing.value = true
// Deal 6 cards (3 to each player)
for (let i = 0; i < 6; i++) {
if (cardsRemaining.value === 0) break
await dealSingleCard()
// Wait 200ms before next card
await new Promise((resolve) => setTimeout(resolve, 700))
}
isDealing.value = false
}
// Reset everything
const reset = () => {
playerHand.value = []
opponentHand.value = []
cardsRemaining.value = 40
flyingCards.value = []
isDealing.value = false
}
</script>
-444
View File
@@ -1,444 +0,0 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-green-700 via-green-800 to-green-900">
<!-- Control Panel (Floating) -->
<div class="fixed top-4 left-4 bg-gray-900/95 rounded-lg p-4 max-w-xs z-50 shadow-2xl">
<h3 class="text-white font-bold mb-3 text-sm">Test Controls</h3>
<div class="space-y-2">
<button
@click="dealInitialCards"
:disabled="isDealing"
class="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white rounded text-xs font-medium transition-colors"
>
Deal Cards
</button>
<button
@click="playRandomCards"
:disabled="playerHand.length === 0"
class="w-full px-3 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white rounded text-xs font-medium transition-colors"
>
Play Both Cards
</button>
<button
@click="collectTrick"
:disabled="!currentTrick.playerCard || !currentTrick.opponentCard"
class="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 text-white rounded text-xs font-medium transition-colors"
>
Collect Trick
</button>
<button
@click="revealTrump = !revealTrump"
class="w-full px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded text-xs font-medium transition-colors"
>
{{ revealTrump ? 'Hide' : 'Reveal' }} Trump
</button>
<button
@click="showGameOver"
class="w-full px-3 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded text-xs font-medium transition-colors"
>
Show Game Over
</button>
<button
@click="resetGame"
class="w-full px-3 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded text-xs font-medium transition-colors"
>
Reset Game
</button>
<div class="pt-2 border-t border-gray-700 mt-2 text-gray-400 text-xs">
<p><strong>Turn:</strong> {{ currentTurn === 'player' ? 'You' : 'Bot' }}</p>
<p><strong>Deck:</strong> {{ cardsRemaining }} cards</p>
</div>
</div>
</div>
<!-- Main Game Board -->
<div
class="grid grid-rows-[auto_auto_1fr_auto] grid-cols-[1fr_3fr_1fr] gap-6 p-8 min-h-screen"
>
<!-- Score Display (Top Left) -->
<div class="col-start-1 col-end-4 row-start-1 flex justify-start items-start">
<ScoreDisplay
:player-score="playerScore"
:opponent-score="opponentScore"
:current-turn="currentTurn"
:round-number="1"
/>
</div>
<!-- Opponent Hand -->
<div class="col-start-2 col-end-3 row-start-2 flex justify-center">
<PlayerHand
ref="opponentHandRef"
:cards="opponentHand"
:face-down="true"
:max-cards="3"
:playable-cards="[]"
/>
</div>
<!-- Play Area -->
<div class="col-start-2 col-end-3 row-start-3 flex items-center justify-center">
<PlayArea
:player-card="currentTrick.playerCard"
:opponent-card="currentTrick.opponentCard"
:winner="currentTrick.winner"
:first-player="currentTrick.firstPlayer"
/>
</div>
<!-- Deck Area -->
<div class="col-start-3 col-end-4 row-start-3 flex items-center justify-center">
<DeckArea
ref="deckRef"
:trump-card="trumpCard"
:cards-remaining="cardsRemaining"
:is-empty="cardsRemaining === 0"
:reveal-trump="revealTrump"
/>
</div>
<!-- Player Hand -->
<div class="col-start-2 col-end-3 row-start-4 flex justify-center">
<PlayerHand
ref="playerHandRef"
:cards="playerHand"
:face-down="false"
:max-cards="3"
:playable-cards="playableCards"
@card-clicked="handleCardClick"
/>
</div>
</div>
<!-- Flying Cards Container -->
<FlyingCard
v-for="flyingCard in flyingCards"
:key="flyingCard.id"
:card="flyingCard.card"
:start-position="flyingCard.startPosition"
:end-position="flyingCard.endPosition"
:duration="500"
:face-down="flyingCard.faceDown"
:delay="flyingCard.delay"
@complete="onFlyingCardComplete(flyingCard)"
/>
<!-- Game Over Modal -->
<GameOver
:is-visible="gameOverVisible"
:winner="gameOverWinner"
:player-score="playerScore"
:opponent-score="opponentScore"
:stats="gameOverStats"
@close="gameOverVisible = false"
@play-again="resetGame"
/>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import GameBoard from '@/components/game/GameBoard.vue'
import ScoreDisplay from '@/components/game/ScoreDisplay.vue'
import PlayerHand from '@/components/game/PlayerHand.vue'
import PlayArea from '@/components/game/PlayArea.vue'
import DeckArea from '@/components/game/DeckArea.vue'
import FlyingCard from '@/components/game/FlyingCard.vue'
import GameOver from '@/components/game/GameOver.vue'
// Component refs
const playerHandRef = ref(null)
const opponentHandRef = ref(null)
const deckRef = ref(null)
// Game state
const playerHand = ref([])
const opponentHand = ref([])
const playerScore = ref(0)
const opponentScore = ref(0)
const currentTurn = ref('player')
const cardsRemaining = ref(40)
const isDealing = ref(false)
const revealTrump = ref(false)
// Trump card
const trumpCard = ref({ suit: 'o', rank: 7 })
// Current trick
const currentTrick = ref({
playerCard: null,
opponentCard: null,
winner: null,
firstPlayer: null,
})
// Flying cards for dealing animation
const flyingCards = ref([])
let flyingCardIdCounter = 0
// Game Over
const gameOverVisible = ref(false)
const gameOverWinner = ref('player')
const gameOverStats = ref({
playerTricks: 0,
opponentTricks: 0,
})
// Sample deck
const sampleCards = [
{ suit: 'c', rank: 1 },
{ suit: 'c', rank: 2 },
{ suit: 'c', rank: 3 },
{ suit: 'c', rank: 4 },
{ suit: 'c', rank: 5 },
{ suit: 'c', rank: 6 },
{ suit: 'c', rank: 7 },
{ suit: 'c', rank: 11 },
{ suit: 'c', rank: 12 },
{ suit: 'c', rank: 13 },
{ suit: 'e', rank: 1 },
{ suit: 'e', rank: 2 },
{ suit: 'e', rank: 3 },
{ suit: 'e', rank: 4 },
{ suit: 'e', rank: 5 },
{ suit: 'e', rank: 6 },
{ suit: 'e', rank: 7 },
{ suit: 'e', rank: 11 },
{ suit: 'e', rank: 12 },
{ suit: 'e', rank: 13 },
{ suit: 'o', rank: 1 },
{ suit: 'o', rank: 2 },
{ suit: 'o', rank: 3 },
{ suit: 'o', rank: 4 },
{ suit: 'o', rank: 5 },
{ suit: 'o', rank: 6 },
{ suit: 'p', rank: 1 },
{ suit: 'p', rank: 2 },
{ suit: 'p', rank: 3 },
{ suit: 'p', rank: 4 },
{ suit: 'p', rank: 5 },
{ suit: 'p', rank: 6 },
{ suit: 'p', rank: 7 },
{ suit: 'p', rank: 11 },
{ suit: 'p', rank: 12 },
{ suit: 'p', rank: 13 },
]
// Playable cards (all cards in hand for testing)
const playableCards = computed(() => {
return playerHand.value.map((_, index) => index)
})
// Get random card
const getRandomCard = () => {
return sampleCards[Math.floor(Math.random() * sampleCards.length)]
}
// Get element position
const getElementPosition = (el) => {
if (!el) return { x: 0, y: 0 }
const rect = el.$el?.getBoundingClientRect() || el.getBoundingClientRect()
return {
x: rect.left + rect.width / 2 - 64,
y: rect.top + rect.height / 2 - 89,
}
}
// Deal a single card
const dealSingleCard = async (isPlayer) => {
if (cardsRemaining.value === 0) return
await nextTick()
const startPos = getElementPosition(deckRef.value)
const endPos = getElementPosition(isPlayer ? playerHandRef.value : opponentHandRef.value)
const card = getRandomCard()
const flyingCard = {
id: flyingCardIdCounter++,
card,
startPosition: startPos,
endPosition: endPos,
faceDown: !isPlayer,
delay: 0,
recipient: isPlayer ? 'player' : 'opponent',
}
flyingCards.value.push(flyingCard)
cardsRemaining.value--
}
// Flying card complete
const onFlyingCardComplete = (flyingCard) => {
if (flyingCard.recipient === 'player') {
playerHand.value.push(flyingCard.card)
} else {
opponentHand.value.push(flyingCard.card)
}
flyingCards.value = flyingCards.value.filter((fc) => fc.id !== flyingCard.id)
}
// Deal initial cards
const dealInitialCards = async () => {
if (isDealing.value) return
isDealing.value = true
// Reveal trump at start
revealTrump.value = true
await new Promise((resolve) => setTimeout(resolve, 1000))
revealTrump.value = false
// Deal 6 cards (3 to each player)
for (let i = 0; i < 6; i++) {
if (cardsRemaining.value === 0) break
const isPlayer = i % 2 === 0
await dealSingleCard(isPlayer)
await new Promise((resolve) => setTimeout(resolve, 700))
}
isDealing.value = false
}
// Handle card click from player hand
const handleCardClick = ({ card, index }) => {
if (currentTurn.value !== 'player') return
if (currentTrick.value.playerCard) return
// Set first player if no cards played yet
if (!currentTrick.value.opponentCard) {
currentTrick.value.firstPlayer = 'player'
}
// Play the card
currentTrick.value.playerCard = card
playerHand.value.splice(index, 1)
// Switch turn
currentTurn.value = 'opponent'
// Auto-play opponent card after delay
setTimeout(() => {
if (opponentHand.value.length > 0 && !currentTrick.value.opponentCard) {
playOpponentCard()
}
}, 1000)
}
// Play opponent card
const playOpponentCard = () => {
if (opponentHand.value.length === 0) return
// Set first player if no cards played yet
if (!currentTrick.value.playerCard) {
currentTrick.value.firstPlayer = 'opponent'
}
// Play random card
const randomIndex = Math.floor(Math.random() * opponentHand.value.length)
const card = opponentHand.value[randomIndex]
currentTrick.value.opponentCard = card
opponentHand.value.splice(randomIndex, 1)
// Switch turn
currentTurn.value = 'player'
}
// Play random cards from both players
const playRandomCards = () => {
if (playerHand.value.length === 0) return
// Player plays first
currentTrick.value.firstPlayer = 'player'
const playerCard = playerHand.value[0]
currentTrick.value.playerCard = playerCard
playerHand.value.shift()
// Opponent plays after delay
setTimeout(() => {
if (opponentHand.value.length > 0) {
const opponentCard = opponentHand.value[0]
currentTrick.value.opponentCard = opponentCard
opponentHand.value.shift()
}
}, 600)
}
// Collect trick
const collectTrick = async () => {
if (!currentTrick.value.playerCard || !currentTrick.value.opponentCard) return
// Randomly determine winner
const winner = Math.random() > 0.5 ? 'player' : 'opponent'
currentTrick.value.winner = winner
// Wait to show winner glow
await new Promise((resolve) => setTimeout(resolve, 1500))
// Update score (random points)
const points = Math.floor(Math.random() * 15) + 5
if (winner === 'player') {
playerScore.value += points
gameOverStats.value.playerTricks++
} else {
opponentScore.value += points
gameOverStats.value.opponentTricks++
}
// Clear cards (triggers collection animation)
currentTrick.value.playerCard = null
currentTrick.value.opponentCard = null
// Reset after animation
await new Promise((resolve) => setTimeout(resolve, 600))
currentTrick.value.winner = null
currentTrick.value.firstPlayer = null
// Deal new cards if deck has cards
if (cardsRemaining.value >= 2 && playerHand.value.length < 3) {
await new Promise((resolve) => setTimeout(resolve, 500))
await dealSingleCard(winner === 'player')
await new Promise((resolve) => setTimeout(resolve, 700))
await dealSingleCard(winner === 'opponent')
}
// Switch turn to winner
currentTurn.value = winner
}
// Show game over
const showGameOver = () => {
// Make player win for demo
if (playerScore.value <= opponentScore.value) {
playerScore.value = opponentScore.value + 10
}
gameOverWinner.value =
playerScore.value > opponentScore.value
? 'player'
: opponentScore.value > playerScore.value
? 'opponent'
: 'draw'
gameOverVisible.value = true
}
// Reset game
const resetGame = () => {
playerHand.value = []
opponentHand.value = []
playerScore.value = 0
opponentScore.value = 0
currentTurn.value = 'player'
cardsRemaining.value = 40
currentTrick.value = {
playerCard: null,
opponentCard: null,
winner: null,
firstPlayer: null,
}
flyingCards.value = []
gameOverVisible.value = false
gameOverStats.value = {
playerTricks: 0,
opponentTricks: 0,
}
revealTrump.value = false
}
</script>
+34 -78
View File
@@ -603,65 +603,6 @@
v-else-if="activeTab === 'matches'" v-else-if="activeTab === 'matches'"
class="animate-in fade-in slide-in-from-bottom-2 duration-300" class="animate-in fade-in slide-in-from-bottom-2 duration-300"
> >
<div class="p-4 bg-gray-50 border-b border-gray-300 flex flex-wrap gap-4 items-end">
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
>From</label
>
<input
type="date"
v-model="filters.match_date_from"
@change="resetAndFetch"
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black"
/>
</div>
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
>To</label
>
<input
type="date"
v-model="filters.match_date_to"
@change="resetAndFetch"
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black"
/>
</div>
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
>Variant</label
>
<select
v-model="filters.match_type"
@change="resetAndFetch"
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black min-w-[100px]"
>
<option :value="null">All</option>
<option value="3">Bisca 3</option>
<option value="9">Bisca 9</option>
</select>
</div>
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold uppercase text-gray-400 tracking-widest"
>Outcome</label
>
<select
v-model="filters.match_outcome"
@change="resetAndFetch"
class="bg-white border border-gray-300 p-2 text-xs font-bold uppercase outline-none focus:border-black min-w-[100px]"
>
<option :value="null">All</option>
<option value="win">Win</option>
<option value="draw">Draw</option>
</select>
</div>
<button
@click="clearMatchFilters"
class="text-[10px] font-bold uppercase underline hover:text-gray-600 mb-2 ml-auto"
>
Clear
</button>
</div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<div <div
id="table-scroll-container" id="table-scroll-container"
@@ -670,20 +611,34 @@
<table class="w-full text-left border-collapse sticky-header"> <table class="w-full text-left border-collapse sticky-header">
<thead class="bg-gray-50 border-b border-gray-300"> <thead class="bg-gray-50 border-b border-gray-300">
<tr> <tr>
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <th
class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"
@click="handleSort('date')"
>
Match Date Match Date
<span>{{ filters.match_sort_direction === 'desc' ? ' ↓' : ' ↑' }}</span>
</th> </th>
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
Matchup (P1 vs P2) Matchup (P1 vs P2)
</th> </th>
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <th
class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"
@click="handleSort('type')"
>
Variant Variant
<span v-if="filters.match_type"> (Bisca {{ filters.match_type }})</span>
<span v-else> </span>
</th> </th>
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500">
Outcome Outcome
</th> </th>
<th class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <th
class="p-4 text-[10px] font-bold uppercase tracking-widest text-gray-500"
@click="handleSort('pot')"
>
Total Pot Total Pot
<span v-if="filters.pot"> ({{ filters.pot }})</span>
<span v-else> </span>
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -882,6 +837,8 @@ const filters = reactive({
match_outcome: null, match_outcome: null,
match_date_from: '', match_date_from: '',
match_date_to: '', match_date_to: '',
match_sort_direction: null,
pot: null,
}) })
watch(activeTab, async () => { watch(activeTab, async () => {
@@ -987,19 +944,6 @@ const filteredUsers = computed(() => {
return users.value.filter((user) => user.id !== useAuthStore().currentUserID) return users.value.filter((user) => user.id !== useAuthStore().currentUserID)
}) })
const resetAndFetch = async () => {
resetPagination()
await fetchData()
}
const clearMatchFilters = async () => {
filters.match_type = null
filters.match_outcome = null
filters.match_date_from = ''
filters.match_date_to = ''
await resetAndFetch()
}
const fetchData = async () => { const fetchData = async () => {
if (isLoading.value) return if (isLoading.value) return
const currentTab = activeTab.value const currentTab = activeTab.value
@@ -1039,10 +983,9 @@ const fetchData = async () => {
} else if (currentTab === 'matches') { } else if (currentTab === 'matches') {
const params = { const params = {
page: pagination[currentTab].page, page: pagination[currentTab].page,
sort_direction: filters.match_sort_direction,
type: filters.match_type, type: filters.match_type,
outcome: filters.match_outcome, pot: filters.pot,
date_from: filters.match_date_from || undefined,
date_to: filters.match_date_to || undefined,
} }
response = await apiStore.getAdminMatches(params) response = await apiStore.getAdminMatches(params)
matches.value.push(...response.data.data) matches.value.push(...response.data.data)
@@ -1080,6 +1023,19 @@ const handleSort = async (column) => {
else if (filters.game_type === '9') filters.game_type = '3' else if (filters.game_type === '9') filters.game_type = '3'
else filters.game_type = null else filters.game_type = null
} }
} else if (activeTab.value === 'matches') {
if (column === 'type') {
filters.match_type =
filters.match_type === null ? '9' : filters.match_type === '9' ? '3' : null
} else if (column === 'date') {
filters.pot = null // Reset pot sort when sorting by date
filters.match_sort_direction = filters.match_sort_direction === 'desc' ? 'asc' : 'desc'
} else if (column === 'pot') {
filters.match_sort_direction = null // Reset date sort when sorting by pot
if (filters.pot === null) filters.pot = 'desc'
else if (filters.pot === 'desc') filters.pot = 'asc'
else filters.pot = 'desc'
}
} }
resetPagination() resetPagination()
-35
View File
@@ -526,21 +526,6 @@
</select> </select>
</div> </div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-muted-foreground">Outcome</label>
<select
v-model="filters.outcome"
@change="resetAndFetch"
class="bg-background border rounded-md px-3 py-2 text-sm outline-none"
>
<option value="all">All Results</option>
<option value="win">Win</option>
<option value="loss">Loss</option>
<option value="draw">Draw</option>
<option value="forfeit">Forfeit</option>
</select>
</div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label class="text-xs font-medium text-muted-foreground">From</label> <label class="text-xs font-medium text-muted-foreground">From</label>
<input <input
@@ -577,7 +562,6 @@
class="bg-background border rounded-md px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-purple-500" class="bg-background border rounded-md px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-purple-500"
> >
<option value="began_at">Date</option> <option value="began_at">Date</option>
<option value="outcome">Outcome</option>
<option value="total_time">Duration</option> <option value="total_time">Duration</option>
</select> </select>
@@ -667,21 +651,6 @@
</select> </select>
</div> </div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-muted-foreground">Outcome</label>
<select
v-model="filters.outcome"
@change="resetAndFetch"
class="bg-background border rounded-md px-3 py-2 text-sm outline-none"
>
<option value="all">All Results</option>
<option value="win">Win</option>
<option value="loss">Loss</option>
<option value="draw">Draw</option>
<option value="forfeit">Forfeit</option>
</select>
</div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label class="text-xs font-medium text-muted-foreground">From</label> <label class="text-xs font-medium text-muted-foreground">From</label>
<input <input
@@ -718,7 +687,6 @@
class="bg-background border rounded-md px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-purple-500" class="bg-background border rounded-md px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-purple-500"
> >
<option value="began_at">Date</option> <option value="began_at">Date</option>
<option value="outcome">Outcome</option>
<option value="total_time">Duration</option> <option value="total_time">Duration</option>
</select> </select>
@@ -1084,7 +1052,6 @@ const pagination = ref({
const filters = ref({ const filters = ref({
type: 'all', type: 'all',
variant: 'all', variant: 'all',
outcome: 'all',
startDate: '', startDate: '',
endDate: '', endDate: '',
sort: { sort: {
@@ -1165,7 +1132,6 @@ const fetchData = async () => {
const gameMatchParams = { const gameMatchParams = {
page: state.page, page: state.page,
type: filters.value.variant !== 'all' ? filters.value.variant : undefined, type: filters.value.variant !== 'all' ? filters.value.variant : undefined,
outcome: filters.value.outcome !== 'all' ? filters.value.outcome : undefined,
status: 'Ended', status: 'Ended',
date_from: filters.value.startDate || undefined, date_from: filters.value.startDate || undefined,
date_to: filters.value.endDate || undefined, date_to: filters.value.endDate || undefined,
@@ -1330,7 +1296,6 @@ const clearFilters = () => {
if (isGameOrMatch) { if (isGameOrMatch) {
filters.value.variant = 'all' filters.value.variant = 'all'
filters.value.outcome = 'all'
} else { } else {
filters.value.type = 'all' filters.value.type = 'all'
} }
+2 -37
View File
@@ -1,13 +1,7 @@
import HomePage from '@/pages/home/HomePage.vue' import HomePage from '@/pages/home/HomePage.vue'
import LoginPage from '@/pages/login/LoginPage.vue' import LoginPage from '@/pages/login/LoginPage.vue'
import LaravelPage from '@/pages/testing/LaravelPage.vue'
import WebsocketsPage from '@/pages/testing/WebsocketsPage.vue'
import UserPage from '@/pages/user/UserPage.vue' import UserPage from '@/pages/user/UserPage.vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import TestAnimations from '@/pages/TestAnimations.vue'
import TestDealing from '@/pages/TestDealing.vue'
import TestAllAnimations from '@/pages/TestAllAnimations.vue'
import TestGameBoard from '@/pages/TestGameBoard.vue'
import SinglePlayerGamePage from '@/pages/game/SinglePlayerGamePage.vue' import SinglePlayerGamePage from '@/pages/game/SinglePlayerGamePage.vue'
import MultiplayerGamePage from '@/pages/game/MultiplayerGamePage.vue' import MultiplayerGamePage from '@/pages/game/MultiplayerGamePage.vue'
import MultiplayerMatchPage from '@/pages/game/MultiplayerMatchPage.vue' import MultiplayerMatchPage from '@/pages/game/MultiplayerMatchPage.vue'
@@ -23,7 +17,7 @@ const router = createRouter({
path: '/', path: '/',
name: 'home', name: 'home',
component: HomePage, component: HomePage,
meta: { requiresAdmin: false } meta: { requiresAdmin: false },
}, },
{ {
path: '/login', path: '/login',
@@ -53,7 +47,7 @@ const router = createRouter({
path: '/game/multiplayer/:id', path: '/game/multiplayer/:id',
name: 'multiplayer-game', name: 'multiplayer-game',
component: MultiplayerGamePage, component: MultiplayerGamePage,
meta: { requiresAuth: true } meta: { requiresAuth: true },
}, },
{ {
path: '/match/:id', path: '/match/:id',
@@ -77,35 +71,6 @@ const router = createRouter({
component: AdminPage, component: AdminPage,
meta: { requiresAuth: true, requiresAdmin: true }, meta: { requiresAuth: true, requiresAdmin: true },
}, },
{
path: '/testing',
children: [
{
path: 'laravel',
component: LaravelPage,
},
{
path: 'websockets',
component: WebsocketsPage,
},
{
path: 'animations',
component: TestAnimations,
},
{
path: 'dealing',
component: TestDealing,
},
{
path: 'all-animations',
component: TestAllAnimations,
},
{
path: 'gameboard',
component: TestGameBoard,
},
],
},
], ],
}) })
+2 -5
View File
@@ -181,12 +181,9 @@ export const useAPIStore = defineStore('api', () => {
const getAdminMatches = (params = {}) => { const getAdminMatches = (params = {}) => {
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
page: params.page || 1, page: params.page || 1,
...(params.sort_direction == 'asc' && { sort_direction: 'asc' }),
...(params.type && { type: params.type }), ...(params.type && { type: params.type }),
...(params.outcome && { outcome: params.outcome }), ...(params.pot && { pot: params.pot }),
...(params.date_from && { date_from: params.date_from }),
...(params.date_to && { date_to: params.date_to }),
sort_by: params.sort_by || 'began_at',
sort_direction: params.sort_direction || 'desc',
}).toString() }).toString()
return axios.get(`${API_BASE_URL}/matches?${queryParams}`) return axios.get(`${API_BASE_URL}/matches?${queryParams}`)