-
-
- {isJoined && (
+
+
+
+
- Invite Players
+ {isJoined ? (
+ <>
+
+ Invite Players
+ >
+ ) : (
+ <>
+
+ Share Lobby
+ >
+ )}
- )}
-
- {sessionPlayer && (
-
- )}
-
-
-
-
- {lobby.name}
-
- {isAdmin && }
-
-
-
- Players ({members?.length || 0}/{lobby.maxPlayers || 8})
-
-
- {members?.length === (lobby.maxPlayers || 8)
- ? "Lobby Full"
- : "Waiting..."}
-
-
-
- {members?.map((member, idx) => (
- -
-
- {member?.role === "admin" && (
-
- Admin
-
- )}
-
-
- ))}
-
+
+ {sessionPlayer && (
+
+ )}
+
+
+
+
+ {lobby.name}
+
+ {isAdmin && }
+
+
+
+ Players ({members?.length || 0}/{lobby.maxPlayers || 8})
+
+
+ {members?.length === (lobby.maxPlayers || 8)
+ ? "Lobby Full"
+ : "Waiting..."}
+
+
+
+
+ {members?.map((member, idx) => (
+ -
+
+ {member?.role === "admin" && (
+
+ Admin
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
-
-
-
-
-
+
);
}
diff --git a/src/app/_components/lobby/lobby-player/lobby-player-card.tsx b/src/app/_components/lobby/lobby-player/player-card.tsx
similarity index 94%
rename from src/app/_components/lobby/lobby-player/lobby-player-card.tsx
rename to src/app/_components/lobby/lobby-player/player-card.tsx
index 9b66933..0ee11b0 100644
--- a/src/app/_components/lobby/lobby-player/lobby-player-card.tsx
+++ b/src/app/_components/lobby/lobby-player/player-card.tsx
@@ -2,7 +2,7 @@ import React from "react";
import Avatar from "@/components/avatar";
import { cn } from "@/lib/utils";
import type { Player } from "@/server/db/schema";
-import LobbyPlayerOptions from "./lobby-player-options";
+import LobbyPlayerOptions from "./player-options";
function LobbyPlayerCard({
lobbyId,
diff --git a/src/app/_components/lobby/lobby-player/kick-player-dialog.tsx b/src/app/_components/lobby/lobby-player/player-kick-dialog.tsx
similarity index 100%
rename from src/app/_components/lobby/lobby-player/kick-player-dialog.tsx
rename to src/app/_components/lobby/lobby-player/player-kick-dialog.tsx
diff --git a/src/app/_components/lobby/lobby-player/lobby-player-options.tsx b/src/app/_components/lobby/lobby-player/player-options.tsx
similarity index 97%
rename from src/app/_components/lobby/lobby-player/lobby-player-options.tsx
rename to src/app/_components/lobby/lobby-player/player-options.tsx
index d20692e..81f6283 100644
--- a/src/app/_components/lobby/lobby-player/lobby-player-options.tsx
+++ b/src/app/_components/lobby/lobby-player/player-options.tsx
@@ -23,7 +23,7 @@ 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 KickPlayerDialog from "./player-kick-dialog";
import { api } from "@/trpc/react";
function LobbyPlayerOptions({
player,
diff --git a/src/app/_components/lobby/lobby-provider.tsx b/src/app/_components/lobby/lobby-provider.tsx
new file mode 100644
index 0000000..1c7f0ac
--- /dev/null
+++ b/src/app/_components/lobby/lobby-provider.tsx
@@ -0,0 +1,57 @@
+import { appRoutes } from "@/config/app.routes";
+import { useSessionPlayerStore } from "@/lib/store/current-player-store";
+import { useLobbyStore } from "@/lib/store/lobby-store";
+import type { Lobby, LobbyMember } 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";
+
+function LobbyProvider({
+ children,
+ initialLobby,
+}: {
+ children: React.ReactNode;
+ initialLobby: Lobby;
+}) {
+ const sessionPlayer = useSessionPlayerStore((state) => state.sessionPlayer);
+ const lobby = useLobbyStore((state) => state.lobby);
+ const updateLobby = useLobbyStore((state) => state.updateLobby);
+ const resetLobby = useLobbyStore((state) => state.resetLobby);
+ const setMembers = useLobbyStore((state) => state.setMembers);
+ const addMember = useLobbyStore((state) => state.addMember);
+ const removeMember = useLobbyStore((state) => state.removeMember);
+
+ const router = useRouter();
+ api.lobby.onMemberUpdate.useSubscription(undefined, {
+ onData({ data: _data }) {
+ const joined = _data.joined;
+ if (joined) {
+ const data = _data.membership as LobbyMember;
+ addMember(data);
+ } else {
+ const data = _data.membership as LobbyMemberLeaveEventData;
+ removeMember(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;
+ updateLobby(data.lobby);
+ },
+ });
+ React.useEffect(() => {
+ resetLobby(initialLobby);
+ setMembers(initialLobby?.members ?? []);
+ }, [initialLobby]);
+
+ return children;
+}
+
+export default LobbyProvider;
diff --git a/src/hooks/use-lobby.ts b/src/hooks/use-lobby.ts
deleted file mode 100644
index afee375..0000000
--- a/src/hooks/use-lobby.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-"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
>(
- 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 };
-}
diff --git a/src/lib/store/current-player-store.ts b/src/lib/store/current-player-store.ts
new file mode 100644
index 0000000..9bb0bcc
--- /dev/null
+++ b/src/lib/store/current-player-store.ts
@@ -0,0 +1,12 @@
+import { create } from "zustand";
+import type { Player } from "@/server/db/schema";
+
+type SessionPlayerStore = {
+ sessionPlayer?: Player | null;
+ setSessionPlayer: (player?: Player | null) => void;
+};
+
+export const useSessionPlayerStore = create((set) => ({
+ sessionPlayer: undefined,
+ setSessionPlayer: (player) => set({ sessionPlayer: player }),
+}));
diff --git a/src/lib/store/lobby-store.ts b/src/lib/store/lobby-store.ts
new file mode 100644
index 0000000..4e14d61
--- /dev/null
+++ b/src/lib/store/lobby-store.ts
@@ -0,0 +1,37 @@
+import { create } from "zustand";
+import type { Lobby, LobbyMember } from "@/server/db/schema";
+
+type LobbyStore = {
+ lobby: Lobby;
+ updateLobby: (lobby: Lobby) => void;
+ resetLobby: (lobby?: Lobby) => void;
+ members: Array;
+ setMembers: (members: Array) => void;
+ findMember: (playerId: string) => LobbyMember | null | undefined;
+ addMember: (member: LobbyMember) => void;
+ removeMember: (playerId: string, kicked?: boolean) => void;
+ selectedGame: number;
+ setSelectedGame: (gameId: number) => void;
+};
+
+export const useLobbyStore = create((set, get) => ({
+ lobby: {} as Lobby,
+ members: [],
+ selectedGame: 0,
+ setSelectedGame: (gameId) => set({ selectedGame: gameId }),
+ updateLobby: (lobby) =>
+ set((state) => ({ ...state, ...lobby, members: state.members })),
+ setMembers: (members) => set({ members }),
+ resetLobby: (lobby) => set({ lobby: lobby ?? ({} as Lobby) }),
+ findMember: (playerId) => get().members.find((m) => m.playerId === playerId),
+ addMember: (member) =>
+ set((state) => {
+ if (state.members.find((m) => m.playerId === member.playerId))
+ return state;
+ return { members: [...state.members, member] };
+ }),
+ removeMember: (playerId) =>
+ set((state) => ({
+ members: state.members.filter((m) => m.playerId !== playerId),
+ })),
+}));
diff --git a/src/server/api/routers/lobby.ts b/src/server/api/routers/lobby.ts
index 323a2fc..2494313 100644
--- a/src/server/api/routers/lobby.ts
+++ b/src/server/api/routers/lobby.ts
@@ -16,7 +16,7 @@ import {
redisAsyncIterator,
redisPublish,
} from "@/server/redis/sse-redis";
-import { tracked } from "@trpc/server";
+import { tracked, TRPCError } from "@trpc/server";
import { trcpSubscriptionInput } from "@/lib/validations/trcp";
import type { LobbyMemberLeaveEventData } from "@/server/redis/events";
@@ -140,22 +140,43 @@ export const lobbyRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
if (input.join) {
- const [member] = await ctx.db
- .insert(lobbyMembers)
- .values({
- lobbyId: input.lobbyId,
- playerId: ctx.session.user.id,
- isReady: false,
- role: "player",
- })
- .returning();
- const player = member
- ? await ctx.db.query.players.findFirst({
- where: eq(players.id, member.playerId),
+ try {
+ const [member] = await ctx.db
+ .insert(lobbyMembers)
+ .values({
+ lobbyId: input.lobbyId,
+ playerId: ctx.session.user.id,
+ isReady: false,
+ role: "player",
})
- : undefined;
- if (member) redisPublish("lobby:member:join", { ...member, player });
- return member;
+ .returning();
+ const player = member
+ ? await ctx.db.query.players.findFirst({
+ where: eq(players.id, member.playerId),
+ })
+ : undefined;
+ if (member) redisPublish("lobby:member:join", { ...member, player });
+ return { success: true, member };
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ if (
+ e.message.includes(
+ "duplicate key value violates unique constraint",
+ )
+ )
+ return {
+ knownError: true,
+ succes: false,
+ error: "You can only be in one lobby at a time.",
+ };
+ } else {
+ return {
+ knownError: true,
+ succes: false,
+ error: "Error joining lobby",
+ };
+ }
+ }
} else {
const [member] = await ctx.db
.delete(lobbyMembers)
@@ -168,7 +189,7 @@ export const lobbyRouter = createTRPCRouter({
.returning();
if (member)
redisPublish("lobby:member:leave", { playerId: member.playerId });
- return member;
+ return { success: true, member };
}
}),
// admin mutaions
diff --git a/src/server/api/routers/player.ts b/src/server/api/routers/player.ts
index 13d3993..8d90efd 100644
--- a/src/server/api/routers/player.ts
+++ b/src/server/api/routers/player.ts
@@ -1,13 +1,13 @@
import { eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
-import { players } from "@/server/db/schema";
+import { players, type Player } from "@/server/db/schema";
export const playerRouter = createTRPCRouter({
getBySession: publicProcedure.query(async ({ ctx }) => {
return ctx?.session?.user
- ? await ctx.db.query.players.findFirst({
+ ? ((await ctx.db.query.players.findFirst({
where: eq(players.id, ctx.session.user.id),
- })
+ })) as Player)
: null;
}),
});