removed socket io and extracted lobby form

This commit is contained in:
2025-03-27 19:21:34 +01:00
parent cc1a27e1c9
commit a00116a61d
19 changed files with 138 additions and 712 deletions
+11 -6
View File
@@ -10,10 +10,10 @@ export default async function QuizGameStartPage() {
const session = await auth();
return (
<>
<div className="mb-8 flex flex-col items-center justify-center gap-2 text-center">
<Icons.logo className="animate-puls h-10 w-10 text-purple-700" />
<div className="mb-8 flex flex-col items-center justify-center gap-2 pt-12 text-center">
<Icons.logo className="animate-puls text-primary h-10 w-10" />
<h1 className="bg-gradient-to-r from-yellow-300 via-pink-400 to-yellow-300 bg-clip-text text-5xl font-extrabold tracking-tight text-transparent uppercase">
<h1 className="from-primary to-secondary bg-gradient-to-b bg-clip-text text-5xl font-black tracking-tight text-transparent uppercase">
{appConfig.name}
</h1>
<p className="text-lg font-medium text-indigo-200">
@@ -26,9 +26,14 @@ export default async function QuizGameStartPage() {
{/* Create Lobby Button */}
<div className="space-y-6">
{session ? (
<CreateLobbyDialog className="w-full" />
<CreateLobbyDialog className="text-shadow-primary w-full" />
) : (
<Button size={"xxl"} variant={"party"} asChild>
<Button
size={"xxl"}
variant={"party"}
asChild
className="text-shadow-primary w-full font-black uppercase"
>
<Link href={"/api/auth/signin"}>Sign-in to create a Lobby</Link>
</Button>
)}
@@ -37,7 +42,7 @@ export default async function QuizGameStartPage() {
<Button
size={"xxl"}
variant={"party"}
className="w-full rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700"
className="w-full rounded-xl bg-gradient-to-r from-blue-500 via-blue-600 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700"
>
<Users className="mr-2 h-6 w-6" />
Join Lobby
+9 -66
View File
@@ -1,20 +1,9 @@
"use client";
import React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { lobbyPatchSchema } from "@/lib/validations/lobby";
import { Icons } from "@/components/icons";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
@@ -23,32 +12,10 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { toast } from "sonner";
import { api } from "@/trpc/react";
import { useRouter } from "next/navigation";
import { Icons } from "@/components/icons";
import { cn } from "@/lib/utils";
import LobbyForm from "./lobby/lobby-form";
function CreateLobbyDialog({ className }: { className?: string }) {
const [open, setOpen] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const { mutateAsync } = api.lobby.create.useMutation();
const router = useRouter();
const form = useForm<z.infer<typeof lobbyPatchSchema>>({
resolver: zodResolver(lobbyPatchSchema),
defaultValues: {
name: "",
},
});
async function onSubmit(lobby: z.infer<typeof lobbyPatchSchema>) {
setLoading(true);
const result = await mutateAsync({ lobby });
setOpen(false);
if (result) router.push(`/lobby/${result.id}`);
else toast.error("Something went wrong.");
setLoading(false);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -57,7 +24,7 @@ function CreateLobbyDialog({ className }: { className?: string }) {
size={"xxl"}
variant={"party"}
className={cn(
"border-green-700 bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-green-600/30 hover:from-green-600 hover:to-emerald-700",
"text-shadow-primary text-primary-foreground font-black",
className,
)}
>
@@ -68,38 +35,14 @@ function CreateLobbyDialog({ className }: { className?: string }) {
</Button>
</DialogTrigger>
<DialogContent className="container-bg bg-border/30">
<DialogContent className="container-bg bg-background/10">
<DialogHeader>
<DialogTitle>Create a Lobby</DialogTitle>
<DialogTitle className="text-shadow-primary text-4xl font-black uppercase">
Create Lobby
</DialogTitle>
<DialogDescription></DialogDescription>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
className="placeholder:text-white/50"
placeholder="What is you lobbies name?"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={loading || !form.formState.isDirty}
>
Create Lobby
</Button>
</form>
</Form>
</DialogHeader>
<LobbyForm cb={() => setOpen(false)} />
</DialogContent>
</Dialog>
);
-22
View File
@@ -7,11 +7,6 @@ import UserCard from "@/components/user-card";
import DeleteLobbyDialog from "@/app/_components/delete-lobby-dialog";
import LobbyMembershipDialog from "@/app/_components/lobby-membership-dialog";
import { type Session } from "next-auth";
import { getSocket } from "@/lib/hooks/use-socket";
import {
LOBBY_USER_PRESENCE_EVENT,
LOBBY_USER_PRESENCE_UPDATE_EVENT,
} from "@/server/socket/event-const";
import { Badge } from "@/components/ui/badge";
function LobbyPage({
@@ -27,25 +22,8 @@ function LobbyPage({
const [memberPresence, setMemberPresence] = React.useState<Array<string>>();
const isJoined = members.find((member) => member.id === session?.user.id);
const isOwner = lobby.createdById === session?.user.id;
const socket = session ? getSocket() : undefined;
React.useEffect(() => {
if (!session || !isJoined || !socket) return;
socket.emit(LOBBY_USER_PRESENCE_EVENT, lobby.id, session.user.id, true);
console.log("user joined");
// socket.on(LOBBY_USER_PRESENCE_UPDATE_EVENT, (users) => {
// console.log("presence updated", users);
// setMemberPresence(users);
// });
return () => {
if (!session || !isJoined || !socket) return;
socket.emit(LOBBY_USER_PRESENCE_EVENT, lobby.id, session.user.id, false);
socket.disconnect();
};
}, [socket]);
return (
<div className="container-bg w-full max-w-md space-y-4 p-6">
+81
View File
@@ -0,0 +1,81 @@
"use client";
import type { Lobby } from "@/server/db/schema";
import React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { lobbyPatchSchema } from "@/lib/validations/lobby";
import { toast } from "sonner";
import { api } from "@/trpc/react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
function LobbyForm({
server_lobby,
cb,
}: {
server_lobby?: Lobby;
cb?: () => void;
}) {
const [loading, setLoading] = React.useState(false);
const { mutateAsync: createLobby } = api.lobby.create.useMutation();
const { mutateAsync: updateLobby } = api.lobby.update.useMutation();
const router = useRouter();
const form = useForm<z.infer<typeof lobbyPatchSchema>>({
resolver: zodResolver(lobbyPatchSchema),
defaultValues: {
name: "",
},
});
async function onSubmit(lobby: z.infer<typeof lobbyPatchSchema>) {
setLoading(true);
const existingLobby = Boolean(server_lobby?.id?.length);
const result = existingLobby
? await updateLobby({ lobby, lobbyId: server_lobby?.id! })
: await createLobby({ lobby });
cb?.();
if (result) {
if (!existingLobby) router.push(`/lobby/${result.id}`);
} else toast.error("Something went wrong.");
setLoading(false);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
className="placeholder:text-white/50"
placeholder="What is you lobbies name?"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading || !form.formState.isDirty}>
Create Lobby
</Button>
</form>
</Form>
);
}
export default LobbyForm;
+1 -7
View File
@@ -3,16 +3,10 @@
import React from "react";
import { Button } from "./ui/button";
import Link from "next/link";
import { Home } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
function NavLink({
label,
path,
className,
isLast,
}: NavLink & { className?: string; isLast?: boolean }) {
function NavLink({ label, path, className }: NavLink & { className?: string }) {
const active = usePathname() === path;
return (
<Button
+5 -8
View File
@@ -11,22 +11,19 @@ async function Navbar() {
return (
<nav className="flex w-full items-center justify-center p-4">
<div className="container-bg flex w-full max-w-sm items-center justify-between gap-2 overflow-hidden rounded-full border-2">
<menu className="flex h-full items-center">
<div className="container-bg flex h-10 w-full max-w-sm items-center justify-between gap-2 overflow-hidden rounded-full border-2">
<menu className="flex items-center">
{appConfig.navigation.map((navLink, idx) => (
<li key={idx}>
<NavLink
isLast={idx === appConfig.navigation.length - 1}
{...navLink}
/>
<NavLink {...navLink} />
</li>
))}
</menu>
{/* Login Button or Profile Avatar */}
{session ? (
{session?.user ? (
<UserPopover user={session.user} />
) : (
<Button asChild className="bg-red-800">
<Button asChild className="bg-primary font-black">
<Link href="/api/auth/signin">Sign In</Link>
</Button>
)}
+1 -1
View File
@@ -21,7 +21,7 @@ const buttonVariants = cva(
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 rounded-full",
link: "text-primary underline-offset-4 hover:underline",
party:
"bg-primary text-primary-foreground rounded-xl border-b-4 shadow-lg font-bold hover:translate-y-[0.1rem] hover:border-b-2 ",
"bg-gradient-to-r from-secondary via-primary to-secondary text-shadow-primary text-primary-foreground rounded-xl border-b-4 shadow-lg font-bold hover:translate-y-[0.1rem] hover:border-b-2 ",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
-3
View File
@@ -25,8 +25,6 @@ export const env = createEnv({
* `NEXT_PUBLIC_`.
*/
client: {
NEXT_PUBLIC_SOCKET_URL: z.string().url(),
// NEXT_PUBLIC_CLIENTVAR: z.string(),
},
@@ -39,7 +37,6 @@ export const env = createEnv({
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
DATABASE_URL: process.env.DATABASE_URL,
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
NODE_ENV: process.env.NODE_ENV,
},
/**
-13
View File
@@ -1,13 +0,0 @@
"use client";
import { env } from "@/env";
import io, { Socket } from "socket.io-client";
let socket: Socket | null = null;
export const getSocket = () => {
if (!socket) {
socket = io(env.NEXT_PUBLIC_SOCKET_URL);
}
return socket;
};
+1
View File
@@ -9,4 +9,5 @@ export const lobbyPatchSchema = z.object({
.max(255, {
message: "Name can only contain up to 255 characters",
}),
maxPlayers: z.number().default(0),
});
+5 -17
View File
@@ -16,13 +16,8 @@ export const lobbies = createTable("lobby", (d) => ({
.primaryKey()
.notNull()
.$defaultFn(() => createId()),
name: d
.varchar({ length: 255 })
.notNull(),
maxMembers: d
.integer()
.notNull()
.default(0),
name: d.varchar({ length: 255 }).notNull(),
maxPlayers: d.integer().notNull().default(0),
createdById: d
.varchar({ length: 255 })
.notNull()
@@ -57,15 +52,9 @@ export const lobbyMembers = createTable(
.varchar({ length: 255 })
.notNull()
.references(() => lobbies.id, { onDelete: "cascade" }),
joinedAt: d
.timestamp("created_at", { withTimezone: true })
.notNull(),
role: d
.varchar({ length: 255 })
.notNull(),
isReady: d
.boolean()
.notNull(),
joinedAt: d.timestamp("created_at", { withTimezone: true }).notNull(),
role: d.varchar({ length: 255 }).notNull(),
isReady: d.boolean().notNull(),
}),
(t) => [primaryKey({ columns: [t.lobbyId, t.userId] })],
);
@@ -81,7 +70,6 @@ export const lobbyMembersRelations = relations(lobbyMembers, ({ one }) => ({
}),
}));
export const users = createTable("user", (d) => ({
id: d
.varchar({ length: 255 })
-7
View File
@@ -1,7 +0,0 @@
export const JOIN_LOBBY_EVENT = "joinLobby";
export const LEAVE_LOBBY_EVENT = "leaveLobby";
export const LOBBY_USER_PRESENCE_EVENT = "lobbyPresence";
export const LOBBY_USER_PRESENCE_UPDATE_EVENT = "lobbyPresenceUpdate";
// export const PLAYER_LOBBY_STATUS = "playerLobbyStatus";
-65
View File
@@ -1,65 +0,0 @@
import { type Server as SocketIOServer } from "socket.io";
import {
LOBBY_USER_PRESENCE_EVENT,
JOIN_LOBBY_EVENT,
LEAVE_LOBBY_EVENT,
LOBBY_USER_PRESENCE_UPDATE_EVENT,
} from "./event-const";
import { env } from "@/env";
const log = (message?: any, ...optionalParams: any[]) => {
if (env.NODE_ENV === "development") {
console.log(message, ...optionalParams);
}
};
const lobbyPresenceMap = new Map<string, Array<string>>();
export const initSocketEvents = (io: SocketIOServer) => {
io.on("connection", (socket) => {
log("New client connected:", socket.id);
socket.on(
LOBBY_USER_PRESENCE_EVENT,
(lobbyId: string, userId: string, present: boolean) => {
socket.join(lobbyId);
// if (!lobbyPresenceMap.has(lobbyId)) {
// lobbyPresenceMap.set(lobbyId, []);
// }
// if (present) {
// lobbyPresenceMap.get(lobbyId)?.push(userId);
// } else {
// lobbyPresenceMap
// .get(lobbyId)
// ?.splice(lobbyPresenceMap.get(lobbyId)?.indexOf(userId)!, 1);
// }
// console.log(lobbyPresenceMap);
// socket
// .to(lobbyId)
// .emit(
// LOBBY_USER_PRESENCE_UPDATE_EVENT,
// lobbyPresenceMap.get(lobbyId),
// );
log(
`user ${userId} is (present: ${present}) in lobby room: ${lobbyId}`,
);
},
);
// Comments
// socket.on(DELETE_COMMENT_EVENT, (commentId: string, articleId: string) => {
// log(`Comment deleted: ${commentId} for article: ${articleId}`);
// io.to(articleId).emit(DELETE_COMMENT_EVENT, commentId);
// });
// socket.on(ADD_COMMENT_EVENT, (comment: Comment) => {
// log(`Comment added: ${comment.id} for article: ${comment.articleId}`);
// io.to(comment.articleId).emit(ADD_COMMENT_EVENT, comment);
// });
socket.on("disconnect", () => {
log("Client disconnected:", socket.id);
});
});
};
-37
View File
@@ -1,37 +0,0 @@
import "dotenv/config";
import express from "express";
import http from "http";
import { Server } from "socket.io";
import { initSocketEvents } from "./events";
import { env } from "@/env";
declare global {
var io: Server | undefined;
}
const createSocketServer = () => {
const app = express();
const server = http.createServer(app);
const PORT = 4000;
const origin = "http://localhost:" + PORT;
if (!global.io) {
global.io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
initSocketEvents(global.io);
server.listen(PORT, () => {
console.log(`Socket.IO server running on port ${PORT}`);
});
}
return global.io;
};
export const ioServer = createSocketServer();
+7 -4
View File
@@ -11,10 +11,10 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--primary: oklch(71.76% 0.2017 349.7);
--primary-foreground: oklch(100% 0 349.7);
--secondary: oklch(88.93% 0.143 90.13);
--secondary-foreground: oklch(21.76% 0.0197 108.64);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
@@ -131,4 +131,7 @@
.gradient-bg {
@apply bg-gradient-to-br from-indigo-950 via-purple-900 to-indigo-900;
}
.text-shadow-primary {
text-shadow: 1px 1px 2px var(--primary);
}
}