145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
import { useState } from "react";
|
|
import type { Lobby } from "@lila/shared";
|
|
|
|
const API_URL = (import.meta.env["VITE_API_URL"] as string) || "";
|
|
|
|
type LobbySuccessResponse = { success: true; data: Lobby };
|
|
type LobbyErrorResponse = { success: false; error: string };
|
|
type LobbyApiResponse = LobbySuccessResponse | LobbyErrorResponse;
|
|
|
|
export const Route = createFileRoute("/multiplayer/")({
|
|
component: MultiplayerPage,
|
|
});
|
|
|
|
function MultiplayerPage() {
|
|
const navigate = useNavigate();
|
|
const [joinCode, setJoinCode] = useState("");
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [isJoining, setIsJoining] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleCreate = async (): Promise<void> => {
|
|
setIsCreating(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch(`${API_URL}/api/v1/lobbies`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
});
|
|
const data = (await response.json()) as LobbyApiResponse;
|
|
if (!data.success) {
|
|
setError(data.error);
|
|
return;
|
|
}
|
|
void navigate({
|
|
to: "/multiplayer/lobby/$code",
|
|
params: { code: data.data.code },
|
|
});
|
|
} catch {
|
|
setError("Could not connect to server. Please try again.");
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleJoin = async (): Promise<void> => {
|
|
const code = joinCode.trim().toUpperCase();
|
|
if (!code) {
|
|
setError("Please enter a lobby code.");
|
|
return;
|
|
}
|
|
setIsJoining(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch(`${API_URL}/api/v1/lobbies/${code}/join`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
});
|
|
const data = (await response.json()) as LobbyApiResponse;
|
|
if (!data.success) {
|
|
setError(data.error);
|
|
return;
|
|
}
|
|
void navigate({
|
|
to: "/multiplayer/lobby/$code",
|
|
params: { code: data.data.code },
|
|
});
|
|
} catch {
|
|
setError("Could not connect to server. Please try again.");
|
|
} finally {
|
|
setIsJoining(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-linear-to-b from-purple-100 to-pink-50 flex items-center justify-center p-6">
|
|
<div className="w-full max-w-md rounded-3xl border border-(--color-primary-light) bg-white/50 dark:bg-black/10 backdrop-blur shadow-sm p-8 flex flex-col gap-6">
|
|
<h1 className="text-2xl font-black tracking-tight text-center text-(--color-text)">
|
|
Multiplayer
|
|
</h1>
|
|
|
|
{error && <p className="text-red-500 text-sm text-center">{error}</p>}
|
|
|
|
{/* Create lobby */}
|
|
<div className="flex flex-col gap-2">
|
|
<h2 className="text-lg font-bold text-(--color-text)">
|
|
Create a lobby
|
|
</h2>
|
|
<p className="text-sm text-(--color-text-muted)">
|
|
Start a new game and invite friends with a code.
|
|
</p>
|
|
<button
|
|
className="rounded-2xl bg-(--color-primary) px-4 py-3 text-white font-black hover:bg-(--color-primary-dark) shadow-sm hover:shadow-md transition-all disabled:opacity-50"
|
|
onClick={() => {
|
|
void handleCreate().catch((err) => {
|
|
console.error("Create lobby error:", err);
|
|
});
|
|
}}
|
|
disabled={isCreating || isJoining}
|
|
>
|
|
{isCreating ? "Creating..." : "Create Lobby"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="border-t border-(--color-primary-light) opacity-60" />
|
|
|
|
{/* Join lobby */}
|
|
<div className="flex flex-col gap-2">
|
|
<h2 className="text-lg font-bold text-(--color-text)">Join a lobby</h2>
|
|
<p className="text-sm text-(--color-text-muted)">
|
|
Enter the code shared by your host.
|
|
</p>
|
|
<input
|
|
className="rounded-2xl border border-(--color-primary-light) bg-white/30 dark:bg-black/10 px-4 py-3 text-sm uppercase tracking-widest text-(--color-text) placeholder:text-(--color-text-muted) focus:outline-none focus:ring-2 focus:ring-(--color-primary)"
|
|
placeholder="Enter code (e.g. WOLF42)"
|
|
value={joinCode}
|
|
onChange={(e) => setJoinCode(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
void handleJoin().catch((err) => {
|
|
console.error("Join lobby error:", err);
|
|
});
|
|
}
|
|
}}
|
|
maxLength={10}
|
|
disabled={isCreating || isJoining}
|
|
/>
|
|
<button
|
|
className="rounded-2xl bg-(--color-surface) border border-(--color-primary-light) px-4 py-3 text-(--color-text) font-black hover:bg-white/30 dark:hover:bg-black/10 shadow-sm hover:shadow-md transition-all disabled:opacity-50"
|
|
onClick={() => {
|
|
void handleJoin().catch((err) => {
|
|
console.error("Join lobby error:", err);
|
|
});
|
|
}}
|
|
disabled={isCreating || isJoining || !joinCode.trim()}
|
|
>
|
|
{isJoining ? "Joining..." : "Join Lobby"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|