-.-
This commit is contained in:
parent
681115e291
commit
4009c44841
@ -1 +0,0 @@
|
|||||||
{"version":"7","dialect":"postgresql","entries":[]}
|
|
||||||
@ -29,6 +29,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
"@radix-ui/react-popover": "^1.1.6",
|
"@radix-ui/react-popover": "^1.1.6",
|
||||||
|
"@radix-ui/react-slider": "^1.2.3",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
|
|||||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@ -35,6 +35,9 @@ importers:
|
|||||||
'@radix-ui/react-popover':
|
'@radix-ui/react-popover':
|
||||||
specifier: ^1.1.6
|
specifier: ^1.1.6
|
||||||
version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-slider':
|
||||||
|
specifier: ^1.2.3
|
||||||
|
version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2(@types/react@19.0.12)(react@19.0.0)
|
version: 1.1.2(@types/react@19.0.12)(react@19.0.0)
|
||||||
@ -918,6 +921,9 @@ packages:
|
|||||||
'@petamoriken/float16@3.9.2':
|
'@petamoriken/float16@3.9.2':
|
||||||
resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==}
|
resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==}
|
||||||
|
|
||||||
|
'@radix-ui/number@1.1.0':
|
||||||
|
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.1':
|
'@radix-ui/primitive@1.1.1':
|
||||||
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
|
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
|
||||||
|
|
||||||
@ -1174,6 +1180,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.2.3':
|
||||||
|
resolution: {integrity: sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.1.2':
|
'@radix-ui/react-slot@1.1.2':
|
||||||
resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==}
|
resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1219,6 +1238,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-previous@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-rect@1.1.0':
|
'@radix-ui/react-use-rect@1.1.0':
|
||||||
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3839,6 +3867,8 @@ snapshots:
|
|||||||
|
|
||||||
'@petamoriken/float16@3.9.2': {}
|
'@petamoriken/float16@3.9.2': {}
|
||||||
|
|
||||||
|
'@radix-ui/number@1.1.0': {}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.1': {}
|
'@radix-ui/primitive@1.1.1': {}
|
||||||
|
|
||||||
'@radix-ui/react-alert-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
'@radix-ui/react-alert-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
@ -4102,6 +4132,25 @@ snapshots:
|
|||||||
'@types/react': 19.0.12
|
'@types/react': 19.0.12
|
||||||
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/number': 1.1.0
|
||||||
|
'@radix-ui/primitive': 1.1.1
|
||||||
|
'@radix-ui/react-collection': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0)
|
||||||
|
'@radix-ui/react-context': 1.1.1(@types/react@19.0.12)(react@19.0.0)
|
||||||
|
'@radix-ui/react-direction': 1.1.0(@types/react@19.0.12)(react@19.0.0)
|
||||||
|
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.12)(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.12)(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.12)(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-size': 1.1.0(@types/react@19.0.12)(react@19.0.0)
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.12
|
||||||
|
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.1.2(@types/react@19.0.12)(react@19.0.0)':
|
'@radix-ui/react-slot@1.1.2(@types/react@19.0.12)(react@19.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0)
|
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.12)(react@19.0.0)
|
||||||
@ -4135,6 +4184,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@types/react': 19.0.12
|
||||||
|
|
||||||
|
'@radix-ui/react-use-previous@1.1.0(@types/react@19.0.12)(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.12
|
||||||
|
|
||||||
'@radix-ui/react-use-rect@1.1.0(@types/react@19.0.12)(react@19.0.0)':
|
'@radix-ui/react-use-rect@1.1.0(@types/react@19.0.12)(react@19.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/rect': 1.1.0
|
'@radix-ui/rect': 1.1.0
|
||||||
|
|||||||
BIN
public/game-placeholder.jpg
Normal file
BIN
public/game-placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
@ -15,7 +15,7 @@ async function Page({
|
|||||||
const lobby = await api.lobby.get({ id });
|
const lobby = await api.lobby.get({ id });
|
||||||
if (!lobby) return notFound();
|
if (!lobby) return notFound();
|
||||||
|
|
||||||
return <LobbyPage lobby={lobby} sessionPlayer={sessionPlayer} />;
|
return <LobbyPage initialLobby={lobby} sessionPlayer={sessionPlayer} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|||||||
@ -23,7 +23,7 @@ async function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return <LobbyPage lobby={lobby} sessionPlayer={sessionPlayer} />;
|
return <LobbyPage initialLobby={lobby} sessionPlayer={sessionPlayer} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|||||||
102
src/app/_components/lobby/game-selector.tsx
Normal file
102
src/app/_components/lobby/game-selector.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
type Game = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
minPlayers: number;
|
||||||
|
maxPlayers: number;
|
||||||
|
};
|
||||||
|
const mokGames: Array<Game> = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Mok Game 1",
|
||||||
|
description: "A simple game of strategy and luck",
|
||||||
|
image: "/game-placeholder.jpg",
|
||||||
|
|
||||||
|
maxPlayers: 2,
|
||||||
|
minPlayers: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Mok Game 2",
|
||||||
|
description: "A simple game of strategy and luck",
|
||||||
|
image: "/game-placeholder.jpg",
|
||||||
|
|
||||||
|
maxPlayers: 2,
|
||||||
|
minPlayers: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Mok Game 3",
|
||||||
|
description: "A simple game of strategy and luck",
|
||||||
|
image: "/game-placeholder.jpg",
|
||||||
|
|
||||||
|
maxPlayers: 2,
|
||||||
|
minPlayers: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function GameSelector({
|
||||||
|
selectGame,
|
||||||
|
selectedGame,
|
||||||
|
}: {
|
||||||
|
selectGame?: (gameId: Game["id"]) => void;
|
||||||
|
selectedGame?: Game["id"];
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = React.useState(selectedGame ?? 1);
|
||||||
|
return (
|
||||||
|
<div className="container-bg grid w-full grid-cols-3 gap-4 p-6">
|
||||||
|
{mokGames.map((game) => (
|
||||||
|
<GameCard
|
||||||
|
key={game.id}
|
||||||
|
game={game}
|
||||||
|
selected={selected === game.id}
|
||||||
|
onClick={() => setSelected(game.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const GameCard = ({
|
||||||
|
game,
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
game: Game;
|
||||||
|
selected?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"container-bg group w-full cursor-pointer items-center justify-between space-y-1 overflow-hidden",
|
||||||
|
selected &&
|
||||||
|
"before:bg-primary/40 before:absolute before:inset-0 before:-z-10 before:w-full before:animate-pulse before:rounded-md before:blur-xl",
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="relative h-20 w-full">
|
||||||
|
<Image
|
||||||
|
fill
|
||||||
|
alt="game-image"
|
||||||
|
src={game.image}
|
||||||
|
className={cn(
|
||||||
|
"object-cover opacity-50 transition-all duration-300 ease-in-out group-hover:opacity-90",
|
||||||
|
selected && "opacity-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 px-4 pb-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-meidum text-xl font-bold">{game.name}</h4>
|
||||||
|
<div className="text-xs text-white/75">{game.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GameSelector;
|
||||||
@ -20,6 +20,8 @@ import { api } from "@/trpc/react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { appRoutes } from "@/config/app.routes";
|
import { appRoutes } from "@/config/app.routes";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { getMinMax } from "@/lib/validations/utils";
|
||||||
|
|
||||||
function LobbyForm({
|
function LobbyForm({
|
||||||
server_lobby,
|
server_lobby,
|
||||||
@ -36,7 +38,7 @@ function LobbyForm({
|
|||||||
resolver: zodResolver(lobbyPatchSchema),
|
resolver: zodResolver(lobbyPatchSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: server_lobby?.name ?? "",
|
name: server_lobby?.name ?? "",
|
||||||
maxPlayers: server_lobby?.maxPlayers ?? 0,
|
maxPlayers: server_lobby?.maxPlayers ?? 2,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
async function onSubmit(lobby: z.infer<typeof lobbyPatchSchema>) {
|
async function onSubmit(lobby: z.infer<typeof lobbyPatchSchema>) {
|
||||||
@ -53,6 +55,7 @@ function LobbyForm({
|
|||||||
}
|
}
|
||||||
} else toast.error("Something went wrong.");
|
} else toast.error("Something went wrong.");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
form.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -75,6 +78,32 @@ function LobbyForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="maxPlayers"
|
||||||
|
render={({ field }) => {
|
||||||
|
const { min, max } = getMinMax(lobbyPatchSchema.shape.maxPlayers);
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Max Players
|
||||||
|
<span className="text-xl font-bold">{`${field.value}`}</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Slider
|
||||||
|
className="w-full max-w-xs"
|
||||||
|
defaultValue={[field.value]}
|
||||||
|
max={max ?? 100}
|
||||||
|
min={min ?? 1}
|
||||||
|
step={1}
|
||||||
|
onValueChange={(value) => field.onChange(value[0])}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { api } from "@/trpc/react";
|
import { api } from "@/trpc/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { appRoutes } from "@/config/app.routes";
|
||||||
|
|
||||||
function LobbyMembershipDialog({
|
function LobbyMembershipDialog({
|
||||||
lobbyId,
|
lobbyId,
|
||||||
@ -26,12 +28,16 @@ function LobbyMembershipDialog({
|
|||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const membership = api.lobby.membership.useMutation();
|
const membership = api.lobby.membership.useMutation();
|
||||||
const labelText = join ? "join" : "leave";
|
const labelText = join ? "join" : "leave";
|
||||||
|
const router = useRouter();
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await membership.mutateAsync({ lobbyId, join });
|
const result = await membership.mutateAsync({ lobbyId, join });
|
||||||
if (result) {
|
if (result) {
|
||||||
toast.success(`Successfully ${labelText} the lobby.`);
|
toast.success(`Successfully ${labelText} the lobby.`);
|
||||||
|
if (join) router.push(appRoutes.currentlobby);
|
||||||
|
else router.push(appRoutes.lobby(lobbyId));
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
} else toast.error("Something went wrong");
|
} else toast.error("Something went wrong");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,79 +1,88 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Lobby, LobbyMember, Player } from "@/server/db/schema";
|
import type { Lobby, Player } from "@/server/db/schema";
|
||||||
import LobbyPlayerCard from "@/app/_components/lobby/lobby_player-card";
|
import LobbyPlayerCard from "@/app/_components/lobby/lobby-player/lobby-player-card";
|
||||||
import DeleteLobbyDialog from "@/app/_components/lobby/delete-lobby-dialog";
|
|
||||||
import LobbyMembershipDialog from "@/app/_components/lobby/lobby-membership-dialog";
|
import LobbyMembershipDialog from "@/app/_components/lobby/lobby-membership-dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { api } from "@/trpc/react";
|
|
||||||
import CopyToClip from "@/components/copy-to-clip";
|
import CopyToClip from "@/components/copy-to-clip";
|
||||||
import { appRoutes } from "@/config/app.routes";
|
import { appRoutes } from "@/config/app.routes";
|
||||||
import LobbySettingsDialog from "./lobby-settings-dialog";
|
import LobbySettingsDialog from "./lobby-settings-dialog";
|
||||||
|
import { useRealtimeLobby } from "@/hooks/use-lobby";
|
||||||
|
import { getBaseUrl } from "@/lib/utils";
|
||||||
|
import GameSelector from "./game-selector";
|
||||||
|
|
||||||
function LobbyPage({
|
function LobbyPage({
|
||||||
sessionPlayer,
|
sessionPlayer,
|
||||||
|
initialLobby,
|
||||||
lobby,
|
|
||||||
}: {
|
}: {
|
||||||
sessionPlayer?: Player | null;
|
sessionPlayer?: Player | null;
|
||||||
lobby: Lobby;
|
initialLobby: Lobby;
|
||||||
}) {
|
}) {
|
||||||
const [members, setMembers] = React.useState<Array<LobbyMember>>(
|
const { members, lobby } = useRealtimeLobby({ initialLobby, sessionPlayer });
|
||||||
lobby?.members ?? [],
|
|
||||||
);
|
|
||||||
api.lobby.onMemberUpdate.useSubscription(undefined, {
|
|
||||||
onData({ data }) {
|
|
||||||
if (data.joined)
|
|
||||||
setMembers((prev) => [...prev, data.membership as LobbyMember]);
|
|
||||||
else
|
|
||||||
setMembers((prev) =>
|
|
||||||
prev.filter((m) => m.playerId !== data.membership),
|
|
||||||
);
|
|
||||||
console.log("Data", data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isJoined = members.find((m) => m.playerId === sessionPlayer?.id);
|
const isJoined = members.find((m) => m.playerId === sessionPlayer?.id);
|
||||||
const isAdmin = isJoined?.role === "admin";
|
const isAdmin = isJoined?.role === "admin";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-bg w-full max-w-md space-y-4 p-6">
|
<div className="grid max-w-4xl grid-cols-3 grid-rows-2 gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="container-bg col-span-2 w-full space-y-4 p-6">
|
||||||
<h1 className="w-full overflow-hidden text-2xl font-bold text-ellipsis capitalize">
|
<div className="flex items-center gap-2">
|
||||||
{lobby.name}
|
{isJoined && (
|
||||||
</h1>
|
<CopyToClip
|
||||||
{isAdmin && <LobbySettingsDialog lobby={lobby} />}
|
target="invite link"
|
||||||
</div>
|
text={getBaseUrl() + appRoutes.lobby(lobby.id)}
|
||||||
|
buttonProps={{ variant: "outline" }}
|
||||||
<ul className="space-y-2">
|
>
|
||||||
{members?.map((member, idx) => (
|
Invite Players
|
||||||
<li key={idx}>
|
</CopyToClip>
|
||||||
<LobbyPlayerCard player={member?.player!} className="relative">
|
|
||||||
{member?.role === "admin" && (
|
|
||||||
<Badge className="absolute -top-2 right-2 p-px px-2 text-white">
|
|
||||||
Admin
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</LobbyPlayerCard>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isJoined && (
|
|
||||||
<CopyToClip
|
|
||||||
target="invite link"
|
|
||||||
text={appRoutes.lobby(lobby.id)}
|
|
||||||
buttonProps={{ variant: "outline" }}
|
|
||||||
>
|
|
||||||
Invite Players
|
|
||||||
</CopyToClip>
|
|
||||||
)}
|
|
||||||
<div className="ml-auto">
|
|
||||||
{sessionPlayer && (
|
|
||||||
<LobbyMembershipDialog lobbyId={lobby.id} join={!isJoined} />
|
|
||||||
)}
|
)}
|
||||||
|
<div className="ml-auto">
|
||||||
|
{sessionPlayer && (
|
||||||
|
<LobbyMembershipDialog lobbyId={lobby.id} join={!isJoined} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex w-full items-center justify-between text-nowrap">
|
||||||
|
<h1 className="w-full overflow-hidden text-2xl font-bold text-ellipsis">
|
||||||
|
{lobby.name}
|
||||||
|
</h1>
|
||||||
|
{isAdmin && <LobbySettingsDialog lobby={lobby} />}
|
||||||
|
</div>
|
||||||
|
<div className="border-border/20 flex items-center justify-between border-b pb-4">
|
||||||
|
<h2 className="text-sm font-medium">
|
||||||
|
Players ({members?.length || 0}/{lobby.maxPlayers || 8})
|
||||||
|
</h2>
|
||||||
|
<span className="text-xs">
|
||||||
|
{members?.length === (lobby.maxPlayers || 8)
|
||||||
|
? "Lobby Full"
|
||||||
|
: "Waiting..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{members?.map((member, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
<LobbyPlayerCard
|
||||||
|
lobbyId={lobby.id}
|
||||||
|
highlight={member?.player?.id === sessionPlayer?.id}
|
||||||
|
player={member?.player!}
|
||||||
|
className="relative"
|
||||||
|
showOptions={isAdmin && member?.playerId !== sessionPlayer?.id}
|
||||||
|
>
|
||||||
|
{member?.role === "admin" && (
|
||||||
|
<Badge className="absolute -top-2 right-2 z-20 p-px px-2 text-white">
|
||||||
|
Admin
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</LobbyPlayerCard>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="container-bg col-span-1 size-full"></div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<GameSelector />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
import type { Player } from "@/server/db/schema";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { ShieldQuestion, UserX } from "lucide-react";
|
||||||
|
import { api } from "@/trpc/react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
function KickPlayerDialog({
|
||||||
|
player,
|
||||||
|
lobbyId,
|
||||||
|
}: {
|
||||||
|
player: Pick<Player, "id" | "displayName" | "avatar">;
|
||||||
|
lobbyId: string;
|
||||||
|
}) {
|
||||||
|
const kickPlayer = api.lobby.kick.useMutation();
|
||||||
|
const handleAction = () => {
|
||||||
|
kickPlayer.mutate({ lobbyId: lobbyId, playerId: player.id });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger className="flex items-center gap-1">
|
||||||
|
<UserX className="group-focus:text-destructive size-4 text-white" />
|
||||||
|
Kick Player
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-1">
|
||||||
|
Are you absolutely sure to
|
||||||
|
<span className="text-destructive font-bold">
|
||||||
|
kick {player.displayName}
|
||||||
|
</span>
|
||||||
|
<ShieldQuestion className="text-destructive size-4" />
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will remove {player.displayName}
|
||||||
|
from the lobby.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant={"destructive"}
|
||||||
|
disabled={kickPlayer.isPending}
|
||||||
|
>
|
||||||
|
<AlertDialogAction onClick={handleAction}>
|
||||||
|
{kickPlayer.isPending ? "Kicking..." : "Kick Player"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KickPlayerDialog;
|
||||||
@ -1,23 +1,30 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Avatar from "../../../components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
|
||||||
import { Button } from "../../../components/ui/button";
|
|
||||||
import type { Player } from "@/server/db/schema";
|
import type { Player } from "@/server/db/schema";
|
||||||
|
import LobbyPlayerOptions from "./lobby-player-options";
|
||||||
|
|
||||||
function LobbyPlayerCard({
|
function LobbyPlayerCard({
|
||||||
|
lobbyId,
|
||||||
player,
|
player,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
highlight,
|
||||||
|
showOptions = false,
|
||||||
}: {
|
}: {
|
||||||
player: Pick<Player, "displayName" | "avatar">;
|
lobbyId: string;
|
||||||
|
player: Player;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
showOptions?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"container-bg flex items-center justify-between gap-2 p-4",
|
"container-bg flex items-center justify-between gap-2 p-4",
|
||||||
|
highlight &&
|
||||||
|
"before:bg-primary/40 before:absolute before:inset-0 before:-z-10 before:w-1/3 before:animate-pulse before:rounded-md before:blur-xl",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -26,13 +33,7 @@ function LobbyPlayerCard({
|
|||||||
<h4 className="font-meidum text-xl">{player.displayName}</h4>
|
<h4 className="font-meidum text-xl">{player.displayName}</h4>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
<Button
|
{showOptions && <LobbyPlayerOptions lobbyId={lobbyId} player={player} />}
|
||||||
size={"icon"}
|
|
||||||
variant={"ghost"}
|
|
||||||
className="hover:bg-border/10 hover:text-white"
|
|
||||||
>
|
|
||||||
<MoreHorizontal />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Ban,
|
||||||
|
Eye,
|
||||||
|
Handshake,
|
||||||
|
HeartHandshake,
|
||||||
|
MoreHorizontal,
|
||||||
|
UserCog,
|
||||||
|
UserX,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import type { Player } from "@/server/db/schema";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { appRoutes } from "@/config/app.routes";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import KickPlayerDialog from "./kick-player-dialog";
|
||||||
|
import { api } from "@/trpc/react";
|
||||||
|
function LobbyPlayerOptions({
|
||||||
|
player,
|
||||||
|
lobbyId,
|
||||||
|
}: {
|
||||||
|
player: Player;
|
||||||
|
lobbyId: string;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const dropdownItemClassName = " flex items-center gap-1 ";
|
||||||
|
const changeRole = api.lobby.changeRole.useMutation();
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size={"icon"}
|
||||||
|
variant={"ghost"}
|
||||||
|
className="hover:bg-border/10 hover:text-white"
|
||||||
|
>
|
||||||
|
<MoreHorizontal />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel className="w-32 truncate overflow-hidden font-bold">
|
||||||
|
{player.displayName}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className={dropdownItemClassName} asChild>
|
||||||
|
<Link href={appRoutes.playerProfile(player.id)} target="_blank">
|
||||||
|
<Eye className="size-4 text-white" />
|
||||||
|
View Profile
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className={dropdownItemClassName} asChild>
|
||||||
|
<Link href={appRoutes.friend(player.id)} target="_blank">
|
||||||
|
<HeartHandshake className="size-4 text-white" />
|
||||||
|
My Friend
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className={dropdownItemClassName}>
|
||||||
|
<UserCog className="size-4 text-white" />
|
||||||
|
Change Role
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
className={cn(
|
||||||
|
dropdownItemClassName,
|
||||||
|
"group focus:border-destructive focus:text-destructive border border-transparent focus:font-bold",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<KickPlayerDialog lobbyId={lobbyId} player={player} />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LobbyPlayerOptions;
|
||||||
@ -14,8 +14,9 @@ import DeleteLobbyDialog from "./delete-lobby-dialog";
|
|||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
|
|
||||||
function LobbySettingsDialog({ lobby }: { lobby: Lobby }) {
|
function LobbySettingsDialog({ lobby }: { lobby: Lobby }) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant={"ghost"} size={"icon"}>
|
<Button variant={"ghost"} size={"icon"}>
|
||||||
<Settings className="size-4" />
|
<Settings className="size-4" />
|
||||||
@ -25,8 +26,8 @@ function LobbySettingsDialog({ lobby }: { lobby: Lobby }) {
|
|||||||
<DialogTitle>Lobby Settings</DialogTitle>
|
<DialogTitle>Lobby Settings</DialogTitle>
|
||||||
<DialogDescription></DialogDescription>
|
<DialogDescription></DialogDescription>
|
||||||
|
|
||||||
<LobbyForm server_lobby={lobby} />
|
<LobbyForm cb={() => setOpen(false)} server_lobby={lobby} />
|
||||||
<div className="flex items-center">
|
<div className="mt-20 flex items-center">
|
||||||
<div className="bg-muted-foreground h-px w-12" />
|
<div className="bg-muted-foreground h-px w-12" />
|
||||||
<p className="relative z-10 px-2 text-xs">Danger Zone</p>
|
<p className="relative z-10 px-2 text-xs">Danger Zone</p>
|
||||||
<div className="bg-muted-foreground h-px grow" />
|
<div className="bg-muted-foreground h-px grow" />
|
||||||
|
|||||||
@ -15,8 +15,7 @@ import type { Player } from "@/server/db/schema";
|
|||||||
import { appRoutes } from "@/config/app.routes";
|
import { appRoutes } from "@/config/app.routes";
|
||||||
|
|
||||||
function UserPopover({ player }: { player: Player }) {
|
function UserPopover({ player }: { player: Player }) {
|
||||||
const dropdownItemClassName =
|
const dropdownItemClassName = " flex items-center gap-1 ";
|
||||||
"focus:bg-border/30 focus:text-white flex items-center gap-1 ";
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
@ -26,7 +25,7 @@ function UserPopover({ player }: { player: Player }) {
|
|||||||
className="border-border/20 size-10 cursor-pointer border-2"
|
className="border-border/20 size-10 cursor-pointer border-2"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="bg-border/30 container-bg text-white">
|
<DropdownMenuContent>
|
||||||
<DropdownMenuLabel className="w-32 truncate overflow-hidden font-bold">
|
<DropdownMenuLabel className="w-32 truncate overflow-hidden font-bold">
|
||||||
{player.displayName}
|
{player.displayName}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, type ButtonProps } from "./ui/button";
|
import { Button, type ButtonProps } from "./ui/button";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import type { title } from "process";
|
||||||
|
|
||||||
function CopyToClip({
|
function CopyToClip({
|
||||||
text,
|
text,
|
||||||
@ -16,7 +17,7 @@ function CopyToClip({
|
|||||||
}) {
|
}) {
|
||||||
const handleclick = () => {
|
const handleclick = () => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
toast.success(`Copied ${target ?? ""} to clipboard`);
|
toast(<p>Copied {target ? <b>{target}</b> : ""} to clipboard</p>);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Button {...buttonProps} onClick={handleclick}>
|
<Button {...buttonProps} onClick={handleclick}>
|
||||||
|
|||||||
@ -112,7 +112,7 @@ function AlertDialogDescription({
|
|||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
data-slot="alert-dialog-description"
|
data-slot="alert-dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-sm text-white/75", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -42,7 +42,7 @@ function DropdownMenuContent({
|
|||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
"bg-background/30 container-bg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 text-white shadow-md",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -74,7 +74,7 @@ function DropdownMenuItem({
|
|||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-border/30 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:text-white data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Popover({
|
function Popover({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverTrigger({
|
function PopoverTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverContent({
|
function PopoverContent({
|
||||||
@ -30,19 +30,19 @@ function PopoverContent({
|
|||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
"bg-background/30 container-bg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverAnchor({
|
function PopoverAnchor({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
|
|||||||
63
src/components/ui/slider.tsx
Normal file
63
src/components/ui/slider.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Slider({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
|
const _values = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
[value, defaultValue, min, max]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
key={index}
|
||||||
|
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Slider }
|
||||||
@ -1,25 +1,27 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes";
|
||||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
|
richColors
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--normal-bg": "var(--popover)",
|
"--normal-bg":
|
||||||
"--normal-text": "var(--popover-foreground)",
|
"color-mix(in oklab, var(--background), transparent 20%)",
|
||||||
|
"--normal-text": "var(--foreground)",
|
||||||
"--normal-border": "var(--border)",
|
"--normal-border": "var(--border)",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Toaster }
|
export { Toaster };
|
||||||
|
|||||||
@ -8,6 +8,10 @@ export const appRoutes = {
|
|||||||
editProfile: "/me/edit",
|
editProfile: "/me/edit",
|
||||||
playerProfile: (id: string) => `/player/${id}`,
|
playerProfile: (id: string) => `/player/${id}`,
|
||||||
|
|
||||||
|
friend: (playerId: string) => `/me/friend/${playerId}`,
|
||||||
|
|
||||||
|
firendsOverview: "/me/friends",
|
||||||
|
|
||||||
signIn: "/api/auth/signin",
|
signIn: "/api/auth/signin",
|
||||||
signOut: "/api/auth/signout",
|
signOut: "/api/auth/signout",
|
||||||
};
|
};
|
||||||
|
|||||||
50
src/hooks/use-lobby.ts
Normal file
50
src/hooks/use-lobby.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { appRoutes } from "@/config/app.routes";
|
||||||
|
import type { Lobby, LobbyMember, Player } from "@/server/db/schema";
|
||||||
|
import type { LobbyMemberLeaveEventData } from "@/server/redis/events";
|
||||||
|
import { api } from "@/trpc/react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function useRealtimeLobby({
|
||||||
|
initialLobby,
|
||||||
|
sessionPlayer,
|
||||||
|
}: {
|
||||||
|
initialLobby: Lobby;
|
||||||
|
sessionPlayer?: Player | null;
|
||||||
|
}) {
|
||||||
|
const [lobby, setLobby] = React.useState(initialLobby);
|
||||||
|
const [members, setMembers] = React.useState<Array<LobbyMember>>(
|
||||||
|
initialLobby?.members ?? [],
|
||||||
|
);
|
||||||
|
const router = useRouter();
|
||||||
|
api.lobby.onMemberUpdate.useSubscription(undefined, {
|
||||||
|
onData({ data: _data }) {
|
||||||
|
const joined = _data.joined;
|
||||||
|
if (joined) {
|
||||||
|
const data = _data.membership as LobbyMember;
|
||||||
|
|
||||||
|
setMembers((prev) => {
|
||||||
|
if (prev.find((m) => m.playerId === data.playerId)) return prev;
|
||||||
|
return [...prev, data];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const data = _data.membership as LobbyMemberLeaveEventData;
|
||||||
|
setMembers((prev) => prev.filter((m) => m.playerId !== data.playerId));
|
||||||
|
if (data?.kicked && data?.playerId === sessionPlayer?.id) {
|
||||||
|
router.push(appRoutes.lobby(lobby.id));
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.lobby.onUpdate.useSubscription(undefined, {
|
||||||
|
onData({ data }) {
|
||||||
|
if (!data?.lobby) return;
|
||||||
|
setLobby((prev) => ({ ...data.lobby, members: prev.members }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { lobby, members };
|
||||||
|
}
|
||||||
2
src/index.d.ts
vendored
2
src/index.d.ts
vendored
@ -2,5 +2,3 @@ type NavLink = {
|
|||||||
label: string;
|
label: string;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LobbyMemberRole = "player" | "admin";
|
|
||||||
|
|||||||
@ -13,3 +13,9 @@ export const formatDate = (date: Date) =>
|
|||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function getBaseUrl() {
|
||||||
|
if (typeof window !== "undefined") return window.location.origin;
|
||||||
|
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||||
|
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -6,8 +6,11 @@ export const lobbyPatchSchema = z.object({
|
|||||||
.min(2, {
|
.min(2, {
|
||||||
message: "Name must contain at least 2 characters",
|
message: "Name must contain at least 2 characters",
|
||||||
})
|
})
|
||||||
.max(255, {
|
.max(64, {
|
||||||
message: "Name can only contain up to 255 characters",
|
message: "Name can only contain up to 64 characters",
|
||||||
}),
|
}),
|
||||||
maxPlayers: z.number().default(0),
|
maxPlayers: z.number().min(2).max(12).default(2),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const LobbyMemberRoleSchema = z.enum(["admin", "player"]);
|
||||||
|
export type LobbyMemberRole = z.infer<typeof LobbyMemberRoleSchema>;
|
||||||
|
|||||||
7
src/lib/validations/trcp.ts
Normal file
7
src/lib/validations/trcp.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const trcpSubscriptionInput = z
|
||||||
|
.object({
|
||||||
|
lastEventId: z.string().nullish(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
11
src/lib/validations/utils.ts
Normal file
11
src/lib/validations/utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const getMinMax = (schema: z.ZodNumber | z.ZodDefault<z.ZodNumber>) => {
|
||||||
|
const baseSchema =
|
||||||
|
schema instanceof z.ZodDefault ? schema.removeDefault() : schema;
|
||||||
|
const min =
|
||||||
|
baseSchema._def.checks.find((c) => c.kind === "min")?.value ?? null;
|
||||||
|
const max =
|
||||||
|
baseSchema._def.checks.find((c) => c.kind === "max")?.value ?? null;
|
||||||
|
return { min, max };
|
||||||
|
};
|
||||||
@ -1,12 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { lobbies, lobbyMembers, players } from "@/server/db/schema";
|
import { lobbies, lobbyMembers, players, type Lobby } from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
import { lobbyPatchSchema } from "@/lib/validations/lobby";
|
import {
|
||||||
|
LobbyMemberRoleSchema,
|
||||||
|
lobbyPatchSchema,
|
||||||
|
} from "@/lib/validations/lobby";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
combineRedisIterators,
|
combineRedisIterators,
|
||||||
@ -14,6 +17,8 @@ import {
|
|||||||
redisPublish,
|
redisPublish,
|
||||||
} from "@/server/redis/sse-redis";
|
} from "@/server/redis/sse-redis";
|
||||||
import { tracked } from "@trpc/server";
|
import { tracked } from "@trpc/server";
|
||||||
|
import { trcpSubscriptionInput } from "@/lib/validations/trcp";
|
||||||
|
import type { LobbyMemberLeaveEventData } from "@/server/redis/events";
|
||||||
|
|
||||||
export const lobbyRouter = createTRPCRouter({
|
export const lobbyRouter = createTRPCRouter({
|
||||||
// queries
|
// queries
|
||||||
@ -100,7 +105,9 @@ export const lobbyRouter = createTRPCRouter({
|
|||||||
eq(lobbies.createdById, ctx.session.user.id),
|
eq(lobbies.createdById, ctx.session.user.id),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.returning({ id: lobbies.id });
|
.returning();
|
||||||
|
|
||||||
|
if (lobby) redisPublish("lobby:update", lobby);
|
||||||
return lobby;
|
return lobby;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
@ -121,7 +128,6 @@ export const lobbyRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.returning({ id: lobbies.id });
|
.returning({ id: lobbies.id });
|
||||||
|
|
||||||
return lobby;
|
return lobby;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -160,23 +166,81 @@ export const lobbyRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.returning();
|
.returning();
|
||||||
if (member) redisPublish("lobby:member:leave", member.playerId);
|
if (member)
|
||||||
|
redisPublish("lobby:member:leave", { playerId: member.playerId });
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
// admin mutaions
|
||||||
|
changeRole: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
lobbyId: z.string(),
|
||||||
|
playerId: z.string(),
|
||||||
|
role: LobbyMemberRoleSchema,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
console.log("Check if user is admin");
|
||||||
|
|
||||||
|
const [member] = await ctx.db
|
||||||
|
.update(lobbyMembers)
|
||||||
|
.set(input.role === "admin" ? { role: "admin" } : { role: "player" })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(lobbyMembers.lobbyId, input.lobbyId),
|
||||||
|
eq(lobbyMembers.playerId, input.playerId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
if (member) redisPublish("lobby:member:update", member);
|
||||||
|
return member;
|
||||||
|
}),
|
||||||
|
|
||||||
|
kick: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
lobbyId: z.string(),
|
||||||
|
playerId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
console.log("Check if user is admin");
|
||||||
|
|
||||||
|
const [member] = await ctx.db
|
||||||
|
.delete(lobbyMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(lobbyMembers.lobbyId, input.lobbyId),
|
||||||
|
eq(lobbyMembers.playerId, input.playerId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
if (member)
|
||||||
|
redisPublish("lobby:member:leave", {
|
||||||
|
playerId: member.playerId,
|
||||||
|
kicked: true,
|
||||||
|
});
|
||||||
|
return member;
|
||||||
|
}),
|
||||||
|
|
||||||
// subscriptions
|
// subscriptions
|
||||||
onMemberUpdate: protectedProcedure
|
onUpdate: publicProcedure
|
||||||
.input(
|
.input(trcpSubscriptionInput)
|
||||||
z
|
|
||||||
.object({
|
|
||||||
lastEventId: z.string().nullish(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
)
|
|
||||||
.subscription(async function* (opts) {
|
.subscription(async function* (opts) {
|
||||||
// Create an async iterator for our Redis channel.
|
if (opts.input?.lastEventId) {
|
||||||
|
// fetch posts from a database that were missed.
|
||||||
|
}
|
||||||
|
for await (const lobby of redisAsyncIterator("lobby:update")) {
|
||||||
|
yield tracked(lobby?.updatedAt?.toString(), {
|
||||||
|
lobby,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
onMemberUpdate: publicProcedure
|
||||||
|
.input(trcpSubscriptionInput)
|
||||||
|
.subscription(async function* (opts) {
|
||||||
if (opts.input?.lastEventId) {
|
if (opts.input?.lastEventId) {
|
||||||
// fetch posts from a database that were missed.
|
// fetch posts from a database that were missed.
|
||||||
}
|
}
|
||||||
@ -184,15 +248,28 @@ export const lobbyRouter = createTRPCRouter({
|
|||||||
for await (const { event, data: membership } of combineRedisIterators([
|
for await (const { event, data: membership } of combineRedisIterators([
|
||||||
"lobby:member:join",
|
"lobby:member:join",
|
||||||
"lobby:member:leave",
|
"lobby:member:leave",
|
||||||
|
"lobby:member:update",
|
||||||
])) {
|
])) {
|
||||||
const joined = event === "lobby:member:join";
|
switch (event) {
|
||||||
const id =
|
case "lobby:member:join":
|
||||||
typeof membership === "string" ? membership : membership.playerId;
|
yield tracked(String(membership.playerId), {
|
||||||
|
joined: true,
|
||||||
yield tracked(String(id), {
|
membership,
|
||||||
joined,
|
});
|
||||||
membership,
|
break;
|
||||||
});
|
case "lobby:member:leave":
|
||||||
|
const data = membership as LobbyMemberLeaveEventData;
|
||||||
|
yield tracked(String(membership.playerId), {
|
||||||
|
joined: false,
|
||||||
|
membership: data,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
// case "lobby:member:update":
|
||||||
|
// yield tracked(String(membership.playerId), {
|
||||||
|
// membership,
|
||||||
|
// });
|
||||||
|
// break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { relations, sql } from "drizzle-orm";
|
|||||||
import { pgTableCreator, index, primaryKey } from "drizzle-orm/pg-core";
|
import { pgTableCreator, index, primaryKey } from "drizzle-orm/pg-core";
|
||||||
import { type AdapterAccount } from "next-auth/adapters";
|
import { type AdapterAccount } from "next-auth/adapters";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import type { LobbyMemberRole } from "@/lib/validations/lobby";
|
||||||
/**
|
/**
|
||||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||||
* database instance for multiple projects.
|
* database instance for multiple projects.
|
||||||
|
|||||||
6
src/server/redis/events.d.ts
vendored
6
src/server/redis/events.d.ts
vendored
@ -1,6 +1,10 @@
|
|||||||
import type { LobbyMember, Post } from "../db/schema";
|
import type { LobbyMember, Post } from "../db/schema";
|
||||||
|
|
||||||
|
export type LobbyMemberLeaveEventData = { playerId: string; kicked?: boolean };
|
||||||
|
|
||||||
export type PubSubEvents = {
|
export type PubSubEvents = {
|
||||||
|
"lobby:update": Lobby;
|
||||||
|
"lobby:member:update": LobbyMember;
|
||||||
"lobby:member:join": LobbyMember;
|
"lobby:member:join": LobbyMember;
|
||||||
"lobby:member:leave": string;
|
"lobby:member:leave": LobbyMemberLeaveEventData;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,18 +31,19 @@ const redisSubscribe = <T extends keyof PubSubEvents>(
|
|||||||
* @param event - The Redis event to subscribe to.
|
* @param event - The Redis event to subscribe to.
|
||||||
* @returns An async iterator that yields tracked events.
|
* @returns An async iterator that yields tracked events.
|
||||||
*/
|
*/
|
||||||
export function redisAsyncIterator<K extends keyof PubSubEvents>(event: K) {
|
export function redisAsyncIterator<K extends keyof PubSubEvents>(
|
||||||
|
event: K,
|
||||||
|
): AsyncIterableIterator<PubSubEvents[K]> {
|
||||||
return {
|
return {
|
||||||
[Symbol.asyncIterator]() {
|
[Symbol.asyncIterator]() {
|
||||||
return {
|
return this;
|
||||||
next(): Promise<IteratorResult<PubSubEvents[K]>> {
|
},
|
||||||
return new Promise((resolve) => {
|
async next(): Promise<IteratorResult<PubSubEvents[K]>> {
|
||||||
redisSubscribe(event, (data: PubSubEvents[K]) => {
|
return new Promise((resolve) => {
|
||||||
resolve({ value: data, done: false });
|
redisSubscribe(event, (data: PubSubEvents[K]) => {
|
||||||
});
|
resolve({ value: data, done: false });
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
|
--background-transparent: oklch(1 0 0 / 20%);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.145 0 0);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
@ -41,6 +42,7 @@
|
|||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.145 0 0);
|
||||||
|
--background-transparent: oklch(0.145 0 0 / 20%);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.205 0 0);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
@ -80,6 +82,7 @@
|
|||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
|
--color-background-transparent: var(--background-transparent);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
@ -126,7 +129,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container-bg {
|
.container-bg {
|
||||||
@apply border-border/20 bg-border/10 rounded-2xl border text-white shadow-[0_0_15px_rgba(124,58,237,0.5)] backdrop-blur-md transition-all;
|
@apply border-border/20 bg-background-transparent rounded-2xl border text-white shadow-[0_0_15px_rgba(124,58,237,0.5)] backdrop-blur-md transition-all;
|
||||||
}
|
}
|
||||||
.gradient-bg {
|
.gradient-bg {
|
||||||
@apply bg-gradient-to-br from-indigo-950 via-purple-900 to-indigo-900;
|
@apply bg-gradient-to-br from-indigo-950 via-purple-900 to-indigo-900;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import SuperJSON from "superjson";
|
|||||||
|
|
||||||
import { type AppRouter } from "@/server/api/root";
|
import { type AppRouter } from "@/server/api/root";
|
||||||
import { createQueryClient } from "./query-client";
|
import { createQueryClient } from "./query-client";
|
||||||
|
import { getBaseUrl } from "@/lib/utils";
|
||||||
|
|
||||||
let clientQueryClientSingleton: QueryClient | undefined = undefined;
|
let clientQueryClientSingleton: QueryClient | undefined = undefined;
|
||||||
const getQueryClient = () => {
|
const getQueryClient = () => {
|
||||||
@ -84,9 +85,3 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBaseUrl() {
|
|
||||||
if (typeof window !== "undefined") return window.location.origin;
|
|
||||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
|
||||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user