2026-06-04 10:11:00 +03:30
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { motion } from "framer-motion";
|
2026-06-04 21:47:38 +03:30
|
|
|
import { Coins, Lock, Trophy, Users } from "lucide-react";
|
2026-06-04 10:11:00 +03:30
|
|
|
import { useState } from "react";
|
|
|
|
|
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
|
2026-06-06 21:58:54 +03:30
|
|
|
import { CoinsPill } from "@/components/online/CoinsPill";
|
2026-06-04 21:47:38 +03:30
|
|
|
import { MATCH_LEAGUES, leagueById } from "@/lib/online/gamification";
|
2026-06-04 10:11:00 +03:30
|
|
|
import { useOnlineStore } from "@/lib/online-store";
|
2026-06-04 16:28:59 +03:30
|
|
|
import { useSessionStore } from "@/lib/session-store";
|
2026-06-04 10:11:00 +03:30
|
|
|
import { useUIStore } from "@/lib/ui-store";
|
2026-06-06 23:05:52 +03:30
|
|
|
import { useGameStore, hasActiveMatch } from "@/lib/game-store";
|
|
|
|
|
import { pushNotification } from "@/lib/notification-store";
|
2026-06-04 10:11:00 +03:30
|
|
|
import { useI18n } from "@/lib/i18n";
|
|
|
|
|
import { cn } from "@/lib/cn";
|
|
|
|
|
|
2026-06-06 23:05:52 +03:30
|
|
|
/** Block starting a 2nd game while one is running — resume it instead. */
|
|
|
|
|
function guardActiveMatch(): boolean {
|
|
|
|
|
if (!hasActiveMatch()) return false;
|
|
|
|
|
useGameStore.getState().resume();
|
|
|
|
|
useUIStore.getState().goGame("online");
|
|
|
|
|
pushNotification({
|
|
|
|
|
kind: "system",
|
|
|
|
|
titleFa: "بازی در جریان",
|
|
|
|
|
titleEn: "Game in progress",
|
|
|
|
|
bodyFa: "ابتدا بازی فعلی را تمام کنید یا تسلیم شوید.",
|
|
|
|
|
bodyEn: "Finish or forfeit your current game first.",
|
|
|
|
|
icon: "🎮",
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 10:11:00 +03:30
|
|
|
export function OnlineLobbyScreen() {
|
2026-06-04 21:47:38 +03:30
|
|
|
const { t, locale } = useI18n();
|
2026-06-04 10:11:00 +03:30
|
|
|
const createRoom = useOnlineStore((s) => s.createRoom);
|
|
|
|
|
const startMatchmaking = useOnlineStore((s) => s.startMatchmaking);
|
|
|
|
|
const go = useUIStore((s) => s.go);
|
2026-06-04 21:47:38 +03:30
|
|
|
const profile = useSessionStore((s) => s.profile);
|
|
|
|
|
const coins = profile?.coins ?? 0;
|
|
|
|
|
const level = profile?.level ?? 1;
|
|
|
|
|
const [leagueId, setLeagueId] = useState(MATCH_LEAGUES[0].id);
|
|
|
|
|
const league = leagueById(leagueId);
|
|
|
|
|
const entry = league.entry;
|
|
|
|
|
const lockedLeague = level < league.minLevel;
|
2026-06-04 10:11:00 +03:30
|
|
|
|
2026-06-04 16:28:59 +03:30
|
|
|
// Private rooms with friends are free.
|
2026-06-04 10:11:00 +03:30
|
|
|
const onCreate = async () => {
|
2026-06-06 23:05:52 +03:30
|
|
|
if (guardActiveMatch()) return;
|
2026-06-04 16:28:59 +03:30
|
|
|
await createRoom({ targetScore: 7, stake: 0, ranked: false });
|
2026-06-04 10:11:00 +03:30
|
|
|
go("room");
|
|
|
|
|
};
|
2026-06-04 16:28:59 +03:30
|
|
|
|
|
|
|
|
// Ranked random always costs the entry (you stake it).
|
2026-06-04 10:11:00 +03:30
|
|
|
const onRandom = async () => {
|
2026-06-06 23:05:52 +03:30
|
|
|
if (guardActiveMatch()) return;
|
2026-06-04 21:47:38 +03:30
|
|
|
if (lockedLeague) return;
|
2026-06-04 16:28:59 +03:30
|
|
|
if (coins < entry) {
|
|
|
|
|
go("buycoins");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await startMatchmaking({ ranked: true, stake: entry });
|
2026-06-04 10:11:00 +03:30
|
|
|
go("matchmaking");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ScreenShell>
|
2026-06-06 21:58:54 +03:30
|
|
|
<ScreenHeader title={t("lobby.title")} right={<CoinsPill />} />
|
2026-06-04 10:11:00 +03:30
|
|
|
|
2026-06-04 21:47:38 +03:30
|
|
|
{/* league pick (only for ranked) */}
|
2026-06-04 10:11:00 +03:30
|
|
|
<div className="glass rounded-2xl p-4 mb-4">
|
|
|
|
|
<div className="flex items-center gap-1.5 text-sm text-cream/70 mb-2.5">
|
2026-06-04 21:47:38 +03:30
|
|
|
<Trophy className="size-4 text-gold-400" />
|
|
|
|
|
{t("lobby.chooseLeague")}
|
2026-06-04 10:11:00 +03:30
|
|
|
</div>
|
2026-06-04 21:47:38 +03:30
|
|
|
<div className="space-y-2">
|
|
|
|
|
{MATCH_LEAGUES.map((l) => {
|
|
|
|
|
const locked = level < l.minLevel;
|
|
|
|
|
const active = l.id === leagueId;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={l.id}
|
|
|
|
|
disabled={locked}
|
|
|
|
|
onClick={() => setLeagueId(l.id)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"w-full rounded-2xl p-3 flex items-center gap-3 border text-start transition",
|
|
|
|
|
active
|
|
|
|
|
? "border-gold-500/70 bg-gold-500/10"
|
|
|
|
|
: "border-navy-700/60 bg-navy-900/50 hover:border-navy-600",
|
|
|
|
|
locked && "opacity-50 cursor-not-allowed"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className="size-10 rounded-xl flex items-center justify-center text-xl shrink-0"
|
|
|
|
|
style={{ background: l.color + "22" }}
|
|
|
|
|
>
|
|
|
|
|
{l.icon}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="flex-1 min-w-0">
|
|
|
|
|
<span className="block text-sm font-black text-cream">
|
|
|
|
|
{locale === "fa" ? l.nameFa : l.nameEn}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="block text-[11px] text-cream/55">
|
|
|
|
|
{locale === "fa" ? l.descFa : l.descEn}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
{locked ? (
|
|
|
|
|
<span className="text-[11px] text-rose-300 flex items-center gap-1 shrink-0">
|
|
|
|
|
<Lock className="size-3.5" />
|
|
|
|
|
{t("lobby.lvl")} {l.minLevel}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="flex items-center gap-1 text-gold-300 font-black text-sm shrink-0">
|
|
|
|
|
{l.entry.toLocaleString()}
|
|
|
|
|
<Coins className="size-3.5" />
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-06-04 10:11:00 +03:30
|
|
|
</div>
|
2026-06-04 21:47:38 +03:30
|
|
|
{!lockedLeague && coins < entry && (
|
2026-06-04 16:28:59 +03:30
|
|
|
<p className="text-rose-300 text-xs mt-2 text-center">{t("lobby.needCoins")}</p>
|
|
|
|
|
)}
|
2026-06-04 10:11:00 +03:30
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<motion.button
|
2026-06-05 10:07:51 +03:30
|
|
|
whileTap={{ scale: 0.985 }}
|
2026-06-04 10:11:00 +03:30
|
|
|
onClick={onRandom}
|
2026-06-05 10:07:51 +03:30
|
|
|
className="press-3d btn-gold w-full rounded-3xl p-5 flex items-center gap-4 text-start"
|
2026-06-04 10:11:00 +03:30
|
|
|
>
|
2026-06-05 10:07:51 +03:30
|
|
|
<span className="grid size-12 place-items-center rounded-2xl bg-black/15 text-[#2a1f04]">
|
2026-06-04 10:11:00 +03:30
|
|
|
<Trophy className="size-6" />
|
|
|
|
|
</span>
|
2026-06-04 16:28:59 +03:30
|
|
|
<span className="flex-1">
|
2026-06-04 10:11:00 +03:30
|
|
|
<span className="block text-lg font-black text-[#2a1f04]">{t("lobby.random")}</span>
|
|
|
|
|
<span className="block text-xs text-[#2a1f04]/70">{t("lobby.randomDesc")}</span>
|
|
|
|
|
</span>
|
2026-06-04 16:28:59 +03:30
|
|
|
<span className="flex items-center gap-1 text-[#2a1f04] font-black">
|
|
|
|
|
{entry}
|
|
|
|
|
<Coins className="size-4" />
|
|
|
|
|
</span>
|
2026-06-04 10:11:00 +03:30
|
|
|
</motion.button>
|
|
|
|
|
|
|
|
|
|
<motion.button
|
2026-06-05 10:07:51 +03:30
|
|
|
whileTap={{ scale: 0.985 }}
|
2026-06-04 10:11:00 +03:30
|
|
|
onClick={onCreate}
|
2026-06-05 10:07:51 +03:30
|
|
|
className="press-3d glass w-full rounded-3xl p-5 flex items-center gap-4 text-start"
|
2026-06-04 10:11:00 +03:30
|
|
|
>
|
2026-06-05 10:07:51 +03:30
|
|
|
<span className="grid size-12 place-items-center rounded-2xl bg-teal-500/15 text-teal-300">
|
2026-06-04 10:11:00 +03:30
|
|
|
<Users className="size-6" />
|
|
|
|
|
</span>
|
2026-06-04 16:28:59 +03:30
|
|
|
<span className="flex-1">
|
2026-06-04 10:11:00 +03:30
|
|
|
<span className="block text-lg font-black text-cream">{t("lobby.createRoom")}</span>
|
|
|
|
|
<span className="block text-xs text-cream/55">{t("lobby.createDesc")}</span>
|
|
|
|
|
</span>
|
2026-06-04 16:28:59 +03:30
|
|
|
<span className="text-teal-300 font-bold text-sm">{t("lobby.free")}</span>
|
2026-06-04 10:11:00 +03:30
|
|
|
</motion.button>
|
|
|
|
|
</div>
|
|
|
|
|
</ScreenShell>
|
|
|
|
|
);
|
|
|
|
|
}
|