fixed global layout and improved UI

This commit is contained in:
mr-shortman 2025-03-26 21:11:49 +01:00
parent 03137d0d5d
commit 987a917896
27 changed files with 722 additions and 404 deletions

View File

@ -1,10 +1,12 @@
type AppConfig = { type AppConfig = {
name: string; name: string;
description: string;
navigation: Array<NavLink>; navigation: Array<NavLink>;
}; };
export const appConfig: AppConfig = { export const appConfig: AppConfig = {
name: "Game Master", name: "Game Master",
description: "Challenge your friends in the ultimate quiz battle!",
navigation: [ navigation: [
{ {
label: "Home", label: "Home",
@ -16,7 +18,7 @@ export const appConfig: AppConfig = {
}, },
{ {
label: "Games", label: "Games",
path: "/#games", path: "/game",
}, },
], ],
}; };

View File

@ -0,0 +1,7 @@
import React from "react";
function GamesPage() {
return <div>GamesPage</div>;
}
export default GamesPage;

View File

@ -1,12 +1,16 @@
import React from "react"; import React from "react";
import Navbar from "@/components/navbar"; import Navbar from "@/components/navbar";
import Footer from "@/components/footer";
function Layout({ children }: { children: React.ReactNode }) { function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-br from-indigo-900 via-purple-900 to-indigo-800"> <div className="gradient-bg min-h-screen">
<Navbar /> <Navbar />
<div className="container mb-8 flex size-full min-h-screen flex-col items-center space-y-6">
{children} {children}
</div> </div>
<Footer />
</div>
); );
} }

View File

@ -1,20 +1,35 @@
import React from "react"; import React from "react";
import { api } from "@/trpc/server"; import { api } from "@/trpc/server";
import Link from "next/link"; import CreateLobbyDialog from "@/app/_components/create-lobby-dialog";
import LobbyCard from "@/app/_components/lobby-card";
import { Button } from "@/components/ui/button";
async function Page() { async function Page() {
const lobbies = await api.lobby.getAll(); const lobbies = await api.lobby.getAll();
return ( return (
<div> <>
<div className="w-full max-w-md space-y-4">
<div className="flex w-full gap-4">
<CreateLobbyDialog className="grow" />
<Button
variant={"party"}
size={"xxl"}
className="container-bg bg-border/10"
>
Placeholder
</Button>
</div>
<menu> <menu>
{lobbies.map((lobby) => ( {lobbies.map((lobby) => (
<li key={lobby.id}> <li key={lobby.id}>
<Link href={`/lobby/${lobby.id}`}>{lobby.name}</Link> <LobbyCard lobby={lobby} />
</li> </li>
))} ))}
</menu> </menu>
</div> </div>
</>
); );
} }

View File

@ -4,40 +4,29 @@ import { Sparkles, Users, Plus } from "lucide-react";
import CreateLobbyDialog from "../_components/create-lobby-dialog"; import CreateLobbyDialog from "../_components/create-lobby-dialog";
import { auth } from "@/server/auth"; import { auth } from "@/server/auth";
import { Icons } from "@/components/icons"; import { Icons } from "@/components/icons";
import { appConfig } from "@/app.config";
export default async function QuizGameStartPage() { export default async function QuizGameStartPage() {
const session = await auth(); const session = await auth();
return ( return (
<div className="relative z-10 w-full max-w-md px-4"> <>
<div className="mb-8 text-center"> <div className="mb-8 flex flex-col items-center justify-center gap-2 text-center">
<div className="mb-2 flex justify-center">
<Icons.logo className="animate-puls h-10 w-10 text-purple-700" /> <Icons.logo className="animate-puls h-10 w-10 text-purple-700" />
</div>
<h1 className="mb-2 bg-gradient-to-r from-yellow-300 via-pink-400 to-yellow-300 bg-clip-text text-5xl font-extrabold tracking-tight text-transparent"> <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">
QUIZ MASTER {appConfig.name}
</h1> </h1>
<p className="text-lg font-medium text-indigo-200"> <p className="text-lg font-medium text-indigo-200">
Challenge your friends in the ultimate quiz battle! {appConfig.description}
</p> </p>
</div> </div>
{/* Game card */} {/* Game card */}
<div className="transform rounded-2xl border border-white/20 bg-white/10 p-8 shadow-[0_0_15px_rgba(124,58,237,0.5)] backdrop-blur-md transition-all"> <div className="container-bg max-w-md p-8">
{/* Create Lobby Button */} {/* Create Lobby Button */}
<div className="space-y-6"> <div className="space-y-6">
{session ? ( {session ? (
<CreateLobbyDialog> <CreateLobbyDialog className="w-full" />
<Button
size={"xxl"}
variant={"party"}
className="w-full 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"
>
<>
<Plus className="size-6" />
Create Lobby
</>
</Button>
</CreateLobbyDialog>
) : ( ) : (
<Button size={"xxl"} variant={"party"} asChild> <Button size={"xxl"} variant={"party"} asChild>
<Link href={"/api/auth/signin"}>Sign-in to create a Lobby</Link> <Link href={"/api/auth/signin"}>Sign-in to create a Lobby</Link>
@ -45,7 +34,11 @@ export default async function QuizGameStartPage() {
)} )}
{/* Join Lobby Button */} {/* Join Lobby Button */}
<Button className="h-16 w-full rounded-xl border-b-4 border-blue-700 bg-gradient-to-r from-blue-500 to-indigo-600 text-lg font-bold text-white shadow-lg shadow-blue-600/30 transition-all duration-200 hover:translate-y-1 hover:border-b-2 hover:from-blue-600 hover:to-indigo-700"> <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"
>
<Users className="mr-2 h-6 w-6" /> <Users className="mr-2 h-6 w-6" />
Join Lobby Join Lobby
</Button> </Button>
@ -61,19 +54,6 @@ export default async function QuizGameStartPage() {
</div> </div>
</div> </div>
</div> </div>
</>
{/* Footer */}
<div className="mt-8 flex justify-between text-sm text-indigo-300/60">
<Link href="#" className="transition-colors hover:text-indigo-200">
How to Play
</Link>
<Link href="#" className="transition-colors hover:text-indigo-200">
Leaderboard
</Link>
<Link href="#" className="transition-colors hover:text-indigo-200">
Settings
</Link>
</div>
</div>
); );
} }

View File

@ -15,16 +15,18 @@ import {
Sparkles, Sparkles,
} from "lucide-react"; } from "lucide-react";
import { auth } from "@/server/auth"; import { auth } from "@/server/auth";
import Avatar from "@/components/avatar";
import { formatDate } from "@/lib/utils";
export default async function ProfilePage() { export default async function ProfilePage() {
const session = await auth(); const session = await auth();
// Mock user data - in a real app this would come from a database // Mock user data - in a real app this would come from a database
const user = { const user = {
username: "QuizMaster42", ...session?.user,
level: 24, level: 24,
xp: 7840, xp: 7840,
xpToNextLevel: 10000, xpToNextLevel: 10000,
joinDate: "March 2023",
gamesPlayed: 187, gamesPlayed: 187,
gamesWon: 112, gamesWon: 112,
winRate: 59.9, winRate: 59.9,
@ -63,43 +65,44 @@ export default async function ProfilePage() {
const xpProgress = (user.xp / user.xpToNextLevel) * 100; const xpProgress = (user.xp / user.xpToNextLevel) * 100;
return ( return (
<div className="relative flex min-h-screen flex-col overflow-hidden"> <main className="mx-auto w-full max-w-4xl flex-1 px-4 py-8">
{/* Main content */}
<main className="relative z-10 container mx-auto max-w-4xl flex-1 px-4 py-8">
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-3">
{/* Profile Card */} {/* Profile Card */}
<div className="md:col-span-1"> <div className="w-full md:col-span-1">
<div className="flex flex-col items-center rounded-2xl border border-white/20 bg-white/10 p-6 shadow-[0_0_15px_rgba(124,58,237,0.5)] backdrop-blur-md"> <div className="container-bg sticky top-4 flex w-full flex-col items-center p-6">
<div className="relative"> <div className="relative">
<div className="h-32 w-32 rounded-full bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 p-1"> <div className="h-32 w-32 rounded-full bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 p-1">
<div className="flex h-full w-full items-center justify-center overflow-hidden rounded-full bg-indigo-900"> <div className="flex h-full w-full items-center justify-center overflow-hidden rounded-full bg-indigo-900">
<Image <Avatar
src="/placeholder.svg?height=120&width=120" src={user.image!}
alt="Profile" fb={user.name!}
width={120} className="size-full"
height={120}
className="object-cover"
/> />
</div> </div>
</div> </div>
<div className="absolute -right-2 -bottom-2 flex h-10 w-10 items-center justify-center rounded-full border-2 border-indigo-900 bg-yellow-400 font-bold text-indigo-900"> <div className="absolute -right-1 -bottom-1 flex h-10 w-10 items-center justify-center rounded-full border-2 border-indigo-900 bg-yellow-400 font-bold text-indigo-900">
{user.level} {user.level}
</div> </div>
</div> </div>
<h1 className="mt-4 text-2xl font-bold text-white"> <h1 className="mt-4 w-full truncate overflow-hidden text-center text-2xl font-bold text-white">
{user.username} {user.name}
</h1> </h1>
<p className="text-sm text-indigo-200"> <p className="text-sm text-indigo-200">
Member since {user.joinDate} Member since{" "}
{new Date(user?.joinedAt!)?.toLocaleDateString("en-EN", {
year: "numeric",
month: "short",
})}
</p> </p>
{/* XP Progress */} {/* XP Progress */}
<div className="mt-6 w-full"> <div className="mt-6 w-full">
<div className="mb-1 flex justify-between text-xs text-indigo-200"> {/* <div className="mb-1 flex justify-between text-xs text-indigo-200">
<span>XP: {user.xp.toLocaleString()}</span> <span>XP: {user.xp.toLocaleString()}</span>
<span>{user.xpToNextLevel.toLocaleString()}</span> <span>{user.xpToNextLevel.toLocaleString()}</span>
</div> </div> */}
<div className="h-3 w-full overflow-hidden rounded-full bg-indigo-900/50"> <div className="h-3 w-full overflow-hidden rounded-full bg-indigo-900/50">
<div <div
className="h-full rounded-full bg-gradient-to-r from-pink-500 to-yellow-400" className="h-full rounded-full bg-gradient-to-r from-pink-500 to-yellow-400"
@ -113,7 +116,7 @@ export default async function ProfilePage() {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="mt-6 grid w-full grid-cols-3 gap-2"> <div className="mt-6 grid w-full grid-cols-2 gap-2">
<div className="rounded-lg bg-white/5 p-2 text-center"> <div className="rounded-lg bg-white/5 p-2 text-center">
<p className="text-2xl font-bold text-white"> <p className="text-2xl font-bold text-white">
{user.gamesPlayed} {user.gamesPlayed}
@ -121,34 +124,32 @@ export default async function ProfilePage() {
<p className="text-xs text-indigo-200">Games</p> <p className="text-xs text-indigo-200">Games</p>
</div> </div>
<div className="rounded-lg bg-white/5 p-2 text-center"> <div className="rounded-lg bg-white/5 p-2 text-center">
<p className="text-2xl font-bold text-white"> <p className="text-2xl font-bold text-white">{user.gamesWon}</p>
{user.gamesWon}
</p>
<p className="text-xs text-indigo-200">Wins</p> <p className="text-xs text-indigo-200">Wins</p>
</div> </div>
<div className="rounded-lg bg-white/5 p-2 text-center"> <div className="col-span-2 rounded-lg bg-white/5 p-2 text-center">
<p className="text-2xl font-bold text-white"> <p className="text-2xl font-bold text-white">{user.winRate}%</p>
{user.winRate}%
</p>
<p className="text-xs text-indigo-200">Win Rate</p> <p className="text-xs text-indigo-200">Win Rate</p>
</div> </div>
</div> </div>
{session ? ( {session ? (
<Button
asChild
variant={"party"}
className="mt-6 w-full border-purple-700 bg-gradient-to-r from-pink-500 to-purple-600 text-white shadow-lg shadow-purple-600/30 hover:border-b-2 hover:from-pink-600 hover:to-purple-700"
>
<Link href={`/profile/${session.user.id}/edit`}> <Link href={`/profile/${session.user.id}/edit`}>
<Button className="mt-6 w-full rounded-xl border-b-4 border-purple-700 bg-gradient-to-r from-pink-500 to-purple-600 text-white shadow-lg shadow-purple-600/30 transition-all duration-200 hover:translate-y-1 hover:border-b-2 hover:from-pink-600 hover:to-purple-700">
Edit Profile Edit Profile
</Button>
</Link> </Link>
) : ( </Button>
<div></div> ) : null}
)}
</div> </div>
</div> </div>
{/* Achievements and Recent Games */} {/* Achievements and Recent Games */}
<div className="space-y-6 md:col-span-2"> <div className="space-y-6 md:col-span-2">
{/* Achievements */} {/* Achievements */}
<div className="rounded-2xl border border-white/20 bg-white/10 p-6 shadow-[0_0_15px_rgba(124,58,237,0.5)] backdrop-blur-md"> <div className="container-bg p-6">
<div className="mb-4 flex items-center"> <div className="mb-4 flex items-center">
<Award className="mr-2 h-6 w-6 text-yellow-400" /> <Award className="mr-2 h-6 w-6 text-yellow-400" />
<h2 className="text-xl font-bold text-white">Achievements</h2> <h2 className="text-xl font-bold text-white">Achievements</h2>
@ -156,10 +157,7 @@ export default async function ProfilePage() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{user.badges.map((badge, index) => ( {user.badges.map((badge, index) => (
<div <div key={index} className="container-bg flex items-center p-3">
key={index}
className="flex items-center rounded-xl border border-white/10 bg-white/5 p-3"
>
<div className={`${badge.color} mr-3`}> <div className={`${badge.color} mr-3`}>
<badge.icon size={24} /> <badge.icon size={24} />
</div> </div>
@ -183,7 +181,7 @@ export default async function ProfilePage() {
</div> </div>
{/* Stats Overview */} {/* Stats Overview */}
<div className="rounded-2xl border border-white/20 bg-white/10 p-6 shadow-[0_0_15px_rgba(124,58,237,0.5)] backdrop-blur-md"> <div className="container-bg p-6">
<div className="mb-4 flex items-center"> <div className="mb-4 flex items-center">
<BarChart3 className="mr-2 h-6 w-6 text-blue-400" /> <BarChart3 className="mr-2 h-6 w-6 text-blue-400" />
<h2 className="text-xl font-bold text-white">Stats Overview</h2> <h2 className="text-xl font-bold text-white">Stats Overview</h2>
@ -191,9 +189,7 @@ export default async function ProfilePage() {
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="rounded-xl border border-white/10 bg-white/5 p-4"> <div className="rounded-xl border border-white/10 bg-white/5 p-4">
<h3 className="mb-2 text-sm text-indigo-200"> <h3 className="mb-2 text-sm text-indigo-200">Top Categories</h3>
Top Categories
</h3>
<div className="space-y-2"> <div className="space-y-2">
<div> <div>
<div className="mb-1 flex justify-between text-xs"> <div className="mb-1 flex justify-between text-xs">
@ -235,9 +231,7 @@ export default async function ProfilePage() {
</div> </div>
<div className="rounded-xl border border-white/10 bg-white/5 p-4"> <div className="rounded-xl border border-white/10 bg-white/5 p-4">
<h3 className="mb-2 text-sm text-indigo-200"> <h3 className="mb-2 text-sm text-indigo-200">Response Time</h3>
Response Time
</h3>
<div className="flex h-24 items-center justify-center"> <div className="flex h-24 items-center justify-center">
<div className="text-center"> <div className="text-center">
<p className="text-3xl font-bold text-white">3.2s</p> <p className="text-3xl font-bold text-white">3.2s</p>
@ -254,7 +248,7 @@ export default async function ProfilePage() {
</div> </div>
{/* Recent Games */} {/* Recent Games */}
<div className="rounded-2xl border border-white/20 bg-white/10 p-6 shadow-[0_0_15px_rgba(124,58,237,0.5)] backdrop-blur-md"> <div className="container-bg p-6">
<div className="mb-4 flex items-center"> <div className="mb-4 flex items-center">
<Clock className="mr-2 h-6 w-6 text-green-400" /> <Clock className="mr-2 h-6 w-6 text-green-400" />
<h2 className="text-xl font-bold text-white">Recent Games</h2> <h2 className="text-xl font-bold text-white">Recent Games</h2>
@ -288,8 +282,7 @@ export default async function ProfilePage() {
<span className="text-white">{game.position}</span> <span className="text-white">{game.position}</span>
</span> </span>
<span className="text-sm text-indigo-300"> <span className="text-sm text-indigo-300">
Score:{" "} Score: <span className="text-white">{game.score}</span>
<span className="text-white">{game.score}</span>
</span> </span>
</div> </div>
</div> </div>
@ -307,36 +300,5 @@ export default async function ProfilePage() {
</div> </div>
</div> </div>
</main> </main>
{/* Footer */}
<footer className="relative z-10 w-full border-t border-white/10 py-4">
<div className="container mx-auto flex items-center justify-between px-4">
<div className="flex items-center">
<Sparkles className="mr-2 h-5 w-5 text-yellow-400" />
<span className="text-sm text-indigo-200">QUIZ MASTER</span>
</div>
<div className="flex space-x-4">
<Link
href="#"
className="text-xs text-indigo-200 transition-colors hover:text-white"
>
Help
</Link>
<Link
href="#"
className="text-xs text-indigo-200 transition-colors hover:text-white"
>
Privacy
</Link>
<Link
href="#"
className="text-xs text-indigo-200 transition-colors hover:text-white"
>
Terms
</Link>
</div>
</div>
</footer>
</div>
); );
} }

View File

@ -8,7 +8,6 @@ import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -27,8 +26,10 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { api } from "@/trpc/react"; import { api } from "@/trpc/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Icons } from "@/components/icons";
import { cn } from "@/lib/utils";
function CreateLobbyDialog({ children }: { children: React.ReactNode }) { function CreateLobbyDialog({ className }: { className?: string }) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const { mutateAsync } = api.lobby.create.useMutation(); const { mutateAsync } = api.lobby.create.useMutation();
@ -51,9 +52,23 @@ function CreateLobbyDialog({ children }: { children: React.ReactNode }) {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>
<Button
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",
className,
)}
>
<>
<Icons.logo className="size-6" />
Create Lobby
</>
</Button>
</DialogTrigger>
<DialogContent> <DialogContent className="container-bg bg-border/30">
<DialogHeader> <DialogHeader>
<DialogTitle>Create a Lobby</DialogTitle> <DialogTitle>Create a Lobby</DialogTitle>
<DialogDescription></DialogDescription> <DialogDescription></DialogDescription>
@ -67,6 +82,7 @@ function CreateLobbyDialog({ children }: { children: React.ReactNode }) {
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
className="placeholder:text-white/50"
placeholder="What is you lobbies name?" placeholder="What is you lobbies name?"
{...field} {...field}
/> />

View File

@ -0,0 +1,35 @@
import React from "react";
import type { Lobby } from "@/server/db/schema";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import Link from "next/link";
import { formatDate } from "@/lib/utils";
import { ArrowRight, ChevronRight } from "lucide-react";
function LobbyCard({ lobby }: { lobby: Lobby }) {
return (
<Link href={`/lobby/${lobby.id}`}>
<Card className="group border-none bg-transparent p-0 shadow-none">
<div className="container-bg py-4">
<CardHeader className="flex w-full items-center justify-between">
<div className="">
<CardTitle className="text-lg">{lobby.name}</CardTitle>
<CardDescription className="text-xs">
{formatDate(lobby.createdAt)}
</CardDescription>
</div>
<div className="">
<ChevronRight className="text-border/20 size-12 transition-colors duration-150 group-hover:text-white" />
</div>
</CardHeader>
</div>
</Card>
</Link>
);
}
export default LobbyCard;

View File

@ -8,7 +8,11 @@ import DeleteLobbyDialog from "@/app/_components/delete-lobby-dialog";
import LobbyMembershipDialog from "@/app/_components/lobby-membership-dialog"; import LobbyMembershipDialog from "@/app/_components/lobby-membership-dialog";
import { type Session } from "next-auth"; import { type Session } from "next-auth";
import { getSocket } from "@/lib/hooks/use-socket"; import { getSocket } from "@/lib/hooks/use-socket";
import { LOBBY_USER_PRESENCE_EVENT } from "@/server/socket/event-const"; import {
LOBBY_USER_PRESENCE_EVENT,
LOBBY_USER_PRESENCE_UPDATE_EVENT,
} from "@/server/socket/event-const";
import { Badge } from "@/components/ui/badge";
function LobbyPage({ function LobbyPage({
session, session,
@ -20,6 +24,7 @@ function LobbyPage({
initialMembers: Array<{ leader: boolean } & PublicUser>; initialMembers: Array<{ leader: boolean } & PublicUser>;
}) { }) {
const [members, setMembers] = React.useState(initialMembers); const [members, setMembers] = React.useState(initialMembers);
const [memberPresence, setMemberPresence] = React.useState<Array<string>>();
const isJoined = members.find((member) => member.id === session?.user.id); const isJoined = members.find((member) => member.id === session?.user.id);
const isOwner = lobby.createdById === session?.user.id; const isOwner = lobby.createdById === session?.user.id;
const socket = session ? getSocket() : undefined; const socket = session ? getSocket() : undefined;
@ -27,6 +32,13 @@ function LobbyPage({
React.useEffect(() => { React.useEffect(() => {
if (!session || !isJoined || !socket) return; if (!session || !isJoined || !socket) return;
socket.emit(LOBBY_USER_PRESENCE_EVENT, lobby.id, session.user.id, true); 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 () => { return () => {
if (!session || !isJoined || !socket) return; if (!session || !isJoined || !socket) return;
@ -36,17 +48,26 @@ function LobbyPage({
}, [socket]); }, [socket]);
return ( return (
<div> <div className="container-bg w-full max-w-md space-y-4 p-6">
<h1 className="text-2xl font-bold capitalize">{lobby.name}</h1> <h1 className="text-2xl font-bold capitalize">{lobby.name}</h1>
<ul> <ul className="space-y-2">
{members?.map((member, idx) => ( {members?.map((member, idx) => (
<li key={idx}> <li key={idx}>
<UserCard name={member.name!} image={member.image!}> <UserCard
{member?.leader && <label>Leader</label>} name={member.name!}
image={member.image!}
className="relative"
>
{member?.leader && (
<Badge className="absolute -top-2 right-2 p-px px-2 text-white">
Leader
</Badge>
)}
</UserCard> </UserCard>
</li> </li>
))} ))}
</ul> </ul>
{JSON.stringify(memberPresence)}
{isOwner && <DeleteLobbyDialog lobbyId={lobby.id} />} {isOwner && <DeleteLobbyDialog lobbyId={lobby.id} />}
{!isOwner && ( {!isOwner && (
<LobbyMembershipDialog lobbyId={lobby.id} join={!isJoined} /> <LobbyMembershipDialog lobbyId={lobby.id} join={!isJoined} />

View File

@ -1,9 +1,3 @@
import { Button, buttonVariants } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
import { import {
@ -14,31 +8,52 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import type { User } from "next-auth"; import type { User } from "next-auth";
import Avatar from "@/components/avatar";
import { Edit, Eye, LogOut } from "lucide-react";
import { cn } from "@/lib/utils";
function UserPopover({ function UserPopover({ user }: { user: User }) {
children, const dropdownItemClassName =
user, "focus:bg-border/30 focus:text-white flex items-center gap-1 ";
}: {
children: React.ReactNode;
user: User;
}) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger>{children}</DropdownMenuTrigger> <DropdownMenuTrigger>
<DropdownMenuContent> <Avatar
<DropdownMenuLabel className="font-bold">{user.name}</DropdownMenuLabel> src={user.image!}
fb={user.name!}
className="border-border/20 size-10 cursor-pointer border-2"
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-border/30 container-bg text-white">
<DropdownMenuLabel className="w-32 truncate overflow-hidden font-bold">
{user.name}
</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuItem className={dropdownItemClassName} asChild>
<Link href={`/profile/${user.id}`}>View Profile</Link> <Link href={`/profile/${user.id}`}>
<Eye className="size-4 text-white" />
View Profile
</Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem className={dropdownItemClassName} asChild>
<Link href={`/profile/${user.id}/edit`}>Edit Profile</Link> <Link href={`/profile/${user.id}/edit`}>
<Edit className="size-4 text-white" />
Edit Profile
</Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem className="bg-destructive text-destructive-foreground hover:bg-destructive/75 hover:text-destructive-foreground"> <DropdownMenuItem
<Link href="/api/auth/signout">Log out</Link> className={cn(
dropdownItemClassName,
"group focus:border-destructive focus:text-destructive border border-transparent focus:font-bold",
)}
asChild
>
<Link href="/api/auth/signout">
<LogOut className="group-focus:text-destructive size-4 text-white" />
Log out
</Link>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

39
src/components/footer.tsx Normal file
View File

@ -0,0 +1,39 @@
import Link from "next/link";
import React from "react";
import { Icons } from "./icons";
import { appConfig } from "@/app.config";
function Footer() {
return (
<footer className="relative z-10 w-full border-t border-white/10 py-4">
<div className="container mx-auto flex items-center justify-between px-4">
<div className="flex items-center">
<Icons.logo className="mr-2 h-5 w-5 text-yellow-400" />
<span className="text-sm text-indigo-200">{appConfig.name}</span>
</div>
<div className="flex space-x-4">
<Link
href="#"
className="text-xs text-indigo-200 transition-colors hover:text-white"
>
Help
</Link>
<Link
href="#"
className="text-xs text-indigo-200 transition-colors hover:text-white"
>
Privacy
</Link>
<Link
href="#"
className="text-xs text-indigo-200 transition-colors hover:text-white"
>
Terms
</Link>
</div>
</div>
</footer>
);
}
export default Footer;

View File

@ -13,14 +13,14 @@ export const Icons = {
{...props} {...props}
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M23.8287 8.94266C22.4425 8.94266 21.0699 9.21493 19.7892 9.74393C18.5086 10.2729 17.3449 11.0483 16.3648 12.0257C15.3846 13.0032 14.6071 14.1636 14.0766 15.4407C13.7048 16.3358 13.4595 17.276 13.346 18.2342C13.2568 18.9873 12.8442 19.6828 12.1656 20.0212L10.1584 21.022C9.47979 21.3604 9.03456 22.0559 9.12376 22.809C9.23726 23.7673 9.48259 24.7074 9.85438 25.6025C10.3849 26.8796 11.1624 28.04 12.1425 29.0175C13.1227 29.9949 14.2864 30.7703 15.567 31.2993C16.8477 31.8283 18.2203 32.1006 19.6064 32.1006C20.9926 32.1006 22.3652 31.8283 23.6459 31.2993C24.9265 30.7703 26.0902 29.9949 27.0704 29.0175C28.0505 28.04 28.828 26.8796 29.3585 25.6025C29.7303 24.7074 29.9756 23.7673 30.0891 22.809C30.1783 22.0559 30.5909 21.3604 31.2696 21.022L33.2767 20.0212C33.9553 19.6828 34.4006 18.9873 34.3114 18.2342C34.1979 17.276 33.9525 16.3358 33.5807 15.4407C33.0503 14.1636 32.2727 13.0032 31.2926 12.0257C30.3124 11.0483 29.1488 10.2729 27.8681 9.74393C26.5874 9.21493 25.2148 8.94266 23.8287 8.94266ZM28.7047 17.4549C28.8111 17.711 28.8968 17.9745 28.9612 18.2428C29.1384 18.9802 28.6775 19.6828 27.9989 20.0212L25.9918 21.022C25.3131 21.3604 24.9161 22.063 24.739 22.8004C24.6745 23.0688 24.5889 23.3322 24.4825 23.5884C24.2172 24.2269 23.8285 24.8071 23.3384 25.2959C22.8483 25.7846 22.2665 26.1723 21.6262 26.4368C20.9858 26.7013 20.2995 26.8374 19.6064 26.8374C18.9134 26.8374 18.2271 26.7013 17.5867 26.4368C16.9464 26.1723 16.3646 25.7846 15.8745 25.2959C15.3844 24.8071 14.9956 24.2269 14.7304 23.5884C14.624 23.3322 14.5384 23.0688 14.4739 22.8004C14.2968 22.063 14.7576 21.3604 15.4362 21.022L17.4433 20.0212C18.122 19.6828 18.519 18.9802 18.6961 18.2428C18.7606 17.9745 18.8463 17.711 18.9526 17.4549C19.2179 16.8163 19.6066 16.2361 20.0967 15.7474C20.5868 15.2586 21.1686 14.871 21.8089 14.6065C22.4493 14.342 23.1356 14.2058 23.8287 14.2058C24.5218 14.2058 25.2081 14.342 25.8484 14.6065C26.4887 14.871 27.0705 15.2586 27.5606 15.7474C28.0507 16.2361 28.4395 16.8163 28.7047 17.4549Z" d="M23.8287 8.94266C22.4425 8.94266 21.0699 9.21493 19.7892 9.74393C18.5086 10.2729 17.3449 11.0483 16.3648 12.0257C15.3846 13.0032 14.6071 14.1636 14.0766 15.4407C13.7048 16.3358 13.4595 17.276 13.346 18.2342C13.2568 18.9873 12.8442 19.6828 12.1656 20.0212L10.1584 21.022C9.47979 21.3604 9.03456 22.0559 9.12376 22.809C9.23726 23.7673 9.48259 24.7074 9.85438 25.6025C10.3849 26.8796 11.1624 28.04 12.1425 29.0175C13.1227 29.9949 14.2864 30.7703 15.567 31.2993C16.8477 31.8283 18.2203 32.1006 19.6064 32.1006C20.9926 32.1006 22.3652 31.8283 23.6459 31.2993C24.9265 30.7703 26.0902 29.9949 27.0704 29.0175C28.0505 28.04 28.828 26.8796 29.3585 25.6025C29.7303 24.7074 29.9756 23.7673 30.0891 22.809C30.1783 22.0559 30.5909 21.3604 31.2696 21.022L33.2767 20.0212C33.9553 19.6828 34.4006 18.9873 34.3114 18.2342C34.1979 17.276 33.9525 16.3358 33.5807 15.4407C33.0503 14.1636 32.2727 13.0032 31.2926 12.0257C30.3124 11.0483 29.1488 10.2729 27.8681 9.74393C26.5874 9.21493 25.2148 8.94266 23.8287 8.94266ZM28.7047 17.4549C28.8111 17.711 28.8968 17.9745 28.9612 18.2428C29.1384 18.9802 28.6775 19.6828 27.9989 20.0212L25.9918 21.022C25.3131 21.3604 24.9161 22.063 24.739 22.8004C24.6745 23.0688 24.5889 23.3322 24.4825 23.5884C24.2172 24.2269 23.8285 24.8071 23.3384 25.2959C22.8483 25.7846 22.2665 26.1723 21.6262 26.4368C20.9858 26.7013 20.2995 26.8374 19.6064 26.8374C18.9134 26.8374 18.2271 26.7013 17.5867 26.4368C16.9464 26.1723 16.3646 25.7846 15.8745 25.2959C15.3844 24.8071 14.9956 24.2269 14.7304 23.5884C14.624 23.3322 14.5384 23.0688 14.4739 22.8004C14.2968 22.063 14.7576 21.3604 15.4362 21.022L17.4433 20.0212C18.122 19.6828 18.519 18.9802 18.6961 18.2428C18.7606 17.9745 18.8463 17.711 18.9526 17.4549C19.2179 16.8163 19.6066 16.2361 20.0967 15.7474C20.5868 15.2586 21.1686 14.871 21.8089 14.6065C22.4493 14.342 23.1356 14.2058 23.8287 14.2058C24.5218 14.2058 25.2081 14.342 25.8484 14.6065C26.4887 14.871 27.0705 15.2586 27.5606 15.7474C28.0507 16.2361 28.4395 16.8163 28.7047 17.4549Z"
fill="currentColor" fill="currentColor"
/> />
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M23.8287 0.521606C21.3336 0.521606 18.8629 1.01169 16.5577 1.96389C14.2525 2.91608 12.158 4.31174 10.3936 6.07116C8.62933 7.83059 7.2298 9.91933 6.27496 12.2181C5.47865 14.1352 5.00464 16.1672 4.86918 18.2322C4.81955 18.989 4.39977 19.6828 3.72111 20.0212L1.714 21.022C1.03534 21.3604 0.597326 22.0543 0.646962 22.811C0.782417 24.8761 1.25643 26.908 2.05273 28.8251C3.00757 31.1239 4.4071 33.2126 6.17142 34.9721C7.93573 36.7315 10.0303 38.1271 12.3355 39.0793C14.6406 40.0315 17.1113 40.5216 19.6064 40.5216C22.1016 40.5216 24.5722 40.0315 26.8774 39.0793C29.1826 38.1271 31.2772 36.7315 33.0415 34.9721C34.8058 33.2126 36.2053 31.1239 37.1602 28.8251C37.9565 26.908 38.4305 24.8761 38.5659 22.811C38.6156 22.0543 39.0353 21.3604 39.714 21.022L41.7211 20.0212C42.3998 19.6828 42.8378 18.989 42.7882 18.2322C42.6527 16.1672 42.1787 14.1352 41.3824 12.2181C40.4275 9.91933 39.028 7.83059 37.2637 6.07116C35.4994 4.31174 33.4048 2.91608 31.0997 1.96389C28.7945 1.01169 26.3238 0.521606 23.8287 0.521606ZM33.2726 22.8102C33.3413 22.0549 33.7576 21.3604 34.4362 21.022L36.4433 20.0212C37.122 19.6828 37.5635 18.9883 37.4948 18.2331C37.3699 16.8597 37.0373 15.5106 36.5064 14.2323C35.8167 12.572 34.806 11.0635 33.5317 9.79278C32.2575 8.52208 30.7448 7.51411 29.0799 6.82641C27.4151 6.13872 25.6307 5.78476 23.8287 5.78476C22.0266 5.78476 20.2423 6.13872 18.5774 6.82641C16.9125 7.51411 15.3998 8.52208 14.1256 9.79278C12.8514 11.0635 11.8406 12.572 11.151 14.2323C10.62 15.5106 10.2874 16.8597 10.1625 18.2331C10.0938 18.9883 9.67755 19.6828 8.99889 20.0212L6.99178 21.022C6.31312 21.3604 5.87162 22.0549 5.9403 22.8102C6.0652 24.1835 6.39777 25.5326 6.92876 26.811C7.61837 28.4712 8.62914 29.9797 9.90337 31.2504C11.1776 32.5211 12.6903 33.5291 14.3552 34.2168C16.02 34.9045 17.8044 35.2584 19.6064 35.2584C21.4085 35.2584 23.1929 34.9045 24.8577 34.2168C26.5226 33.5291 28.0353 32.5211 29.3095 31.2504C30.5838 29.9797 31.5945 28.4712 32.2841 26.811C32.8151 25.5326 33.1477 24.1835 33.2726 22.8102Z" d="M23.8287 0.521606C21.3336 0.521606 18.8629 1.01169 16.5577 1.96389C14.2525 2.91608 12.158 4.31174 10.3936 6.07116C8.62933 7.83059 7.2298 9.91933 6.27496 12.2181C5.47865 14.1352 5.00464 16.1672 4.86918 18.2322C4.81955 18.989 4.39977 19.6828 3.72111 20.0212L1.714 21.022C1.03534 21.3604 0.597326 22.0543 0.646962 22.811C0.782417 24.8761 1.25643 26.908 2.05273 28.8251C3.00757 31.1239 4.4071 33.2126 6.17142 34.9721C7.93573 36.7315 10.0303 38.1271 12.3355 39.0793C14.6406 40.0315 17.1113 40.5216 19.6064 40.5216C22.1016 40.5216 24.5722 40.0315 26.8774 39.0793C29.1826 38.1271 31.2772 36.7315 33.0415 34.9721C34.8058 33.2126 36.2053 31.1239 37.1602 28.8251C37.9565 26.908 38.4305 24.8761 38.5659 22.811C38.6156 22.0543 39.0353 21.3604 39.714 21.022L41.7211 20.0212C42.3998 19.6828 42.8378 18.989 42.7882 18.2322C42.6527 16.1672 42.1787 14.1352 41.3824 12.2181C40.4275 9.91933 39.028 7.83059 37.2637 6.07116C35.4994 4.31174 33.4048 2.91608 31.0997 1.96389C28.7945 1.01169 26.3238 0.521606 23.8287 0.521606ZM33.2726 22.8102C33.3413 22.0549 33.7576 21.3604 34.4362 21.022L36.4433 20.0212C37.122 19.6828 37.5635 18.9883 37.4948 18.2331C37.3699 16.8597 37.0373 15.5106 36.5064 14.2323C35.8167 12.572 34.806 11.0635 33.5317 9.79278C32.2575 8.52208 30.7448 7.51411 29.0799 6.82641C27.4151 6.13872 25.6307 5.78476 23.8287 5.78476C22.0266 5.78476 20.2423 6.13872 18.5774 6.82641C16.9125 7.51411 15.3998 8.52208 14.1256 9.79278C12.8514 11.0635 11.8406 12.572 11.151 14.2323C10.62 15.5106 10.2874 16.8597 10.1625 18.2331C10.0938 18.9883 9.67755 19.6828 8.99889 20.0212L6.99178 21.022C6.31312 21.3604 5.87162 22.0549 5.9403 22.8102C6.0652 24.1835 6.39777 25.5326 6.92876 26.811C7.61837 28.4712 8.62914 29.9797 9.90337 31.2504C11.1776 32.5211 12.6903 33.5291 14.3552 34.2168C16.02 34.9045 17.8044 35.2584 19.6064 35.2584C21.4085 35.2584 23.1929 34.9045 24.8577 34.2168C26.5226 33.5291 28.0353 32.5211 29.3095 31.2504C30.5838 29.9797 31.5945 28.4712 32.2841 26.811C32.8151 25.5326 33.1477 24.1835 33.2726 22.8102Z"
fill="currentColor" fill="currentColor"
/> />

View File

@ -5,13 +5,31 @@ import { Button } from "./ui/button";
import Link from "next/link"; import Link from "next/link";
import { Home } from "lucide-react"; import { Home } from "lucide-react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
function NavLink({ label, path }: NavLink) { function NavLink({
label,
path,
className,
isLast,
}: NavLink & { className?: string; isLast?: boolean }) {
const active = usePathname() === path; const active = usePathname() === path;
return ( return (
<Button asChild variant={active ? "default" : "ghost"}> <Button
asChild
variant={"ghost"}
className={cn(
"group hover:bg-border/20 h-10 rounded-none font-bold text-white/50 hover:text-white/50",
active && "bg-border/20 text-white hover:text-white",
className,
)}
>
<Link href={path}> <Link href={path}>
<span>{label}</span> <span
className={"transition-transform duration-150 group-hover:scale-105"}
>
{label}
</span>
</Link> </Link>
</Button> </Button>
); );

View File

@ -1,9 +1,7 @@
import React from "react"; import React from "react";
import { auth } from "@/server/auth"; import { auth } from "@/server/auth";
import Avatar from "./avatar";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import Link from "next/link"; import Link from "next/link";
import { Home } from "lucide-react";
import UserPopover from "@/app/_components/user-popover"; import UserPopover from "@/app/_components/user-popover";
import { appConfig } from "@/app.config"; import { appConfig } from "@/app.config";
import NavLink from "./nav-link"; import NavLink from "./nav-link";
@ -12,24 +10,21 @@ async function Navbar() {
const session = await auth(); const session = await auth();
return ( return (
<nav className="absolute top-0 left-0 z-50 flex h-max w-full items-center justify-center p-4"> <nav className="flex w-full items-center justify-center p-4">
<div className="bg-background flex w-max items-center gap-2 rounded-full border-2"> <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 items-center"> <menu className="flex h-full items-center">
{appConfig.navigation.map((navLink, idx) => ( {appConfig.navigation.map((navLink, idx) => (
<li key={idx}> <li key={idx}>
<NavLink {...navLink} /> <NavLink
isLast={idx === appConfig.navigation.length - 1}
{...navLink}
/>
</li> </li>
))} ))}
</menu> </menu>
{/* Login Button or Profile Avatar */} {/* Login Button or Profile Avatar */}
{session ? ( {session ? (
<UserPopover user={session.user}> <UserPopover user={session.user} />
<Avatar
src={session.user.image!}
fb={session.user.name!}
className="cursor-pointer"
/>
</UserPopover>
) : ( ) : (
<Button asChild className="bg-red-800"> <Button asChild className="bg-red-800">
<Link href="/api/auth/signin">Sign In</Link> <Link href="/api/auth/signin">Sign In</Link>

View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -21,14 +21,14 @@ const buttonVariants = cva(
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 rounded-full", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 rounded-full",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
party: party:
"bg-primary text-primary-foreground rounded-xl border-b-4 shadow-lg text-lg font-bold hover:translate-y-1 hover:border-b-2 ", "bg-primary text-primary-foreground rounded-xl border-b-4 shadow-lg font-bold hover:translate-y-[0.1rem] hover:border-b-2 ",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-9",
xxl: "h-16 w-full", xxl: "h-16 px-8 text-lg rounded-2xl has-[>svg]:px-8",
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -1,15 +1,15 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
@ -17,7 +17,7 @@ function DropdownMenuPortal({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
) );
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
@ -28,7 +28,7 @@ function DropdownMenuTrigger({
data-slot="dropdown-menu-trigger" data-slot="dropdown-menu-trigger"
{...props} {...props}
/> />
) );
} }
function DropdownMenuContent({ function DropdownMenuContent({
@ -43,12 +43,12 @@ function DropdownMenuContent({
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-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",
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
) );
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
@ -56,7 +56,7 @@ function DropdownMenuGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
) );
} }
function DropdownMenuItem({ function DropdownMenuItem({
@ -65,8 +65,8 @@ function DropdownMenuItem({
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@ -75,11 +75,11 @@ function DropdownMenuItem({
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-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",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
) );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...props} {...props}
/> />
) );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
@ -140,7 +140,7 @@ function DropdownMenuRadioItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
) );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
@ -148,7 +148,7 @@ function DropdownMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
@ -156,11 +156,11 @@ function DropdownMenuLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
@ -173,7 +173,7 @@ function DropdownMenuSeparator({
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
@ -185,17 +185,17 @@ function DropdownMenuShortcut({
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
} }
function DropdownMenuSubTrigger({ function DropdownMenuSubTrigger({
@ -204,7 +204,7 @@ function DropdownMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@ -212,14 +212,14 @@ function DropdownMenuSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
) );
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
@ -231,11 +231,11 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -254,4 +254,4 @@ export {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
} };

View File

@ -1,21 +1,39 @@
import React from "react"; import React from "react";
import Avatar from "./avatar"; import Avatar from "./avatar";
import { cn } from "@/lib/utils";
import { MoreHorizontal } from "lucide-react";
import { Button } from "./ui/button";
function UserCard({ function UserCard({
name, name,
image, image,
children, children,
className,
}: { }: {
name: string; name: string;
image: string; image: string;
children?: React.ReactNode; children?: React.ReactNode;
className?: string;
}) { }) {
return ( return (
<div className="flex items-center"> <div
className={cn(
"container-bg flex items-center justify-between gap-2 p-4",
className,
)}
>
<div className="flex items-center gap-2">
<Avatar src={image} fb={name} /> <Avatar src={image} fb={name} />
<div>
<h4 className="font-meidum text-xl">{name}</h4> <h4 className="font-meidum text-xl">{name}</h4>
</div> </div>
{children}
<Button
size={"icon"}
variant={"ghost"}
className="hover:bg-border/10 hover:text-white"
>
<MoreHorizontal />
</Button>
</div> </div>
); );
} }

View File

@ -1,6 +1,15 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }
export const formatDate = (date: Date) =>
date.toLocaleString("en-EN", {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
});

View File

@ -44,6 +44,7 @@ export const lobbyRouter = createTRPCRouter({
image: true, image: true,
name: true, name: true,
id: true, id: true,
joinedAt: true,
}, },
}, },
members: { members: {
@ -53,6 +54,7 @@ export const lobbyRouter = createTRPCRouter({
image: true, image: true,
name: true, name: true,
id: true, id: true,
joinedAt: true,
}, },
}, },
}, },

View File

@ -9,6 +9,7 @@ import {
users, users,
verificationTokens, verificationTokens,
} from "@/server/db/schema"; } from "@/server/db/schema";
import type { Adapter } from "next-auth/adapters";
/** /**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@ -20,17 +21,19 @@ declare module "next-auth" {
interface Session extends DefaultSession { interface Session extends DefaultSession {
user: { user: {
id: string; id: string;
joinedAt: Date;
// ...other properties // ...other properties
// role: UserRole; // role: UserRole;
} & DefaultSession["user"]; } & DefaultSession["user"];
} }
// interface User { interface User {
// // ...other properties joinedAt: Date;
// // role: UserRole; // ...other properties
// } // role: UserRole;
} }
export type PublicUser = Pick<User, "name" | "image" | "id">; }
export type PublicUser = Pick<User, "name" | "image" | "id" | "joinedAt">;
/** /**
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc. * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
@ -55,7 +58,7 @@ export const authConfig = {
accountsTable: accounts, accountsTable: accounts,
sessionsTable: sessions, sessionsTable: sessions,
verificationTokensTable: verificationTokens, verificationTokensTable: verificationTokens,
}), }) as Adapter,
callbacks: { callbacks: {
session: ({ session, user }) => ({ session: ({ session, user }) => ({
@ -63,6 +66,7 @@ export const authConfig = {
user: { user: {
...session.user, ...session.user,
id: user.id, id: user.id,
joinedAt: user.joinedAt,
}, },
}), }),
}, },

View File

@ -17,6 +17,7 @@ export const lobbies = createTable("lobby", (d) => ({
.notNull() .notNull()
.$defaultFn(() => createId()), .$defaultFn(() => createId()),
name: d.varchar({ length: 255 }), name: d.varchar({ length: 255 }),
maxMembers: d.integer().notNull().default(0),
createdById: d createdById: d
.varchar({ length: 255 }) .varchar({ length: 255 })
@ -82,6 +83,10 @@ export const users = createTable("user", (d) => ({
}) })
.default(sql`CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP`),
image: d.varchar({ length: 255 }), image: d.varchar({ length: 255 }),
joinedAt: d
.timestamp("joined_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
})); }));
export const usersRelations = relations(users, ({ many }) => ({ export const usersRelations = relations(users, ({ many }) => ({

View File

@ -1,6 +1,7 @@
export const JOIN_LOBBY_EVENT = "joinLobby"; export const JOIN_LOBBY_EVENT = "joinLobby";
export const LEAVE_LOBBY_EVENT = "leaveLobby"; export const LEAVE_LOBBY_EVENT = "leaveLobby";
export const LOBBY_USER_PRESENCE_EVENT = "userPresence"; export const LOBBY_USER_PRESENCE_EVENT = "lobbyPresence";
export const LOBBY_USER_PRESENCE_UPDATE_EVENT = "lobbyPresenceUpdate";
// export const PLAYER_LOBBY_STATUS = "playerLobbyStatus"; // export const PLAYER_LOBBY_STATUS = "playerLobbyStatus";

View File

@ -3,6 +3,7 @@ import {
LOBBY_USER_PRESENCE_EVENT, LOBBY_USER_PRESENCE_EVENT,
JOIN_LOBBY_EVENT, JOIN_LOBBY_EVENT,
LEAVE_LOBBY_EVENT, LEAVE_LOBBY_EVENT,
LOBBY_USER_PRESENCE_UPDATE_EVENT,
} from "./event-const"; } from "./event-const";
import { env } from "@/env"; import { env } from "@/env";
@ -12,6 +13,8 @@ const log = (message?: any, ...optionalParams: any[]) => {
} }
}; };
const lobbyPresenceMap = new Map<string, Array<string>>();
export const initSocketEvents = (io: SocketIOServer) => { export const initSocketEvents = (io: SocketIOServer) => {
io.on("connection", (socket) => { io.on("connection", (socket) => {
log("New client connected:", socket.id); log("New client connected:", socket.id);
@ -20,6 +23,24 @@ export const initSocketEvents = (io: SocketIOServer) => {
LOBBY_USER_PRESENCE_EVENT, LOBBY_USER_PRESENCE_EVENT,
(lobbyId: string, userId: string, present: boolean) => { (lobbyId: string, userId: string, present: boolean) => {
socket.join(lobbyId); 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( log(
`user ${userId} is (present: ${present}) in lobby room: ${lobbyId}`, `user ${userId} is (present: ${present}) in lobby room: ${lobbyId}`,
); );

View File

@ -19,7 +19,7 @@ const createSocketServer = () => {
if (!global.io) { if (!global.io) {
global.io = new Server(server, { global.io = new Server(server, {
cors: { cors: {
origin, origin: "*",
methods: ["GET", "POST"], methods: ["GET", "POST"],
}, },
}); });

View File

@ -113,11 +113,22 @@
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
} }
@utility container {
margin-inline: auto;
}
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply text-foreground bg-background;
}
.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;
}
.gradient-bg {
@apply bg-gradient-to-br from-indigo-950 via-purple-900 to-indigo-900;
} }
} }