fixed global layout and improved UI
This commit is contained in:
parent
03137d0d5d
commit
987a917896
@ -1,10 +1,12 @@
|
||||
type AppConfig = {
|
||||
name: string;
|
||||
description: string;
|
||||
navigation: Array<NavLink>;
|
||||
};
|
||||
|
||||
export const appConfig: AppConfig = {
|
||||
name: "Game Master",
|
||||
description: "Challenge your friends in the ultimate quiz battle!",
|
||||
navigation: [
|
||||
{
|
||||
label: "Home",
|
||||
@ -16,7 +18,7 @@ export const appConfig: AppConfig = {
|
||||
},
|
||||
{
|
||||
label: "Games",
|
||||
path: "/#games",
|
||||
path: "/game",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
7
src/app/(routes)/game/page.tsx
Normal file
7
src/app/(routes)/game/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
function GamesPage() {
|
||||
return <div>GamesPage</div>;
|
||||
}
|
||||
|
||||
export default GamesPage;
|
||||
@ -1,11 +1,15 @@
|
||||
import React from "react";
|
||||
import Navbar from "@/components/navbar";
|
||||
import Footer from "@/components/footer";
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
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 />
|
||||
{children}
|
||||
<div className="container mb-8 flex size-full min-h-screen flex-col items-center space-y-6">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,20 +1,35 @@
|
||||
import React from "react";
|
||||
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() {
|
||||
const lobbies = await api.lobby.getAll();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<menu>
|
||||
{lobbies.map((lobby) => (
|
||||
<li key={lobby.id}>
|
||||
<Link href={`/lobby/${lobby.id}`}>{lobby.name}</Link>
|
||||
</li>
|
||||
))}
|
||||
</menu>
|
||||
</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>
|
||||
{lobbies.map((lobby) => (
|
||||
<li key={lobby.id}>
|
||||
<LobbyCard lobby={lobby} />
|
||||
</li>
|
||||
))}
|
||||
</menu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -4,40 +4,29 @@ import { Sparkles, Users, Plus } from "lucide-react";
|
||||
import CreateLobbyDialog from "../_components/create-lobby-dialog";
|
||||
import { auth } from "@/server/auth";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { appConfig } from "@/app.config";
|
||||
|
||||
export default async function QuizGameStartPage() {
|
||||
const session = await auth();
|
||||
return (
|
||||
<div className="relative z-10 w-full max-w-md px-4">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="mb-2 flex justify-center">
|
||||
<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">
|
||||
QUIZ MASTER
|
||||
<>
|
||||
<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" />
|
||||
|
||||
<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">
|
||||
{appConfig.name}
|
||||
</h1>
|
||||
<p className="text-lg font-medium text-indigo-200">
|
||||
Challenge your friends in the ultimate quiz battle!
|
||||
{appConfig.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="space-y-6">
|
||||
{session ? (
|
||||
<CreateLobbyDialog>
|
||||
<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>
|
||||
<CreateLobbyDialog className="w-full" />
|
||||
) : (
|
||||
<Button size={"xxl"} variant={"party"} asChild>
|
||||
<Link href={"/api/auth/signin"}>Sign-in to create a Lobby</Link>
|
||||
@ -45,7 +34,11 @@ export default async function QuizGameStartPage() {
|
||||
)}
|
||||
|
||||
{/* 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" />
|
||||
Join Lobby
|
||||
</Button>
|
||||
@ -61,19 +54,6 @@ export default async function QuizGameStartPage() {
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,16 +15,18 @@ import {
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { auth } from "@/server/auth";
|
||||
import Avatar from "@/components/avatar";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const session = await auth();
|
||||
|
||||
// Mock user data - in a real app this would come from a database
|
||||
const user = {
|
||||
username: "QuizMaster42",
|
||||
...session?.user,
|
||||
level: 24,
|
||||
xp: 7840,
|
||||
xpToNextLevel: 10000,
|
||||
joinDate: "March 2023",
|
||||
gamesPlayed: 187,
|
||||
gamesWon: 112,
|
||||
winRate: 59.9,
|
||||
@ -63,280 +65,240 @@ export default async function ProfilePage() {
|
||||
const xpProgress = (user.xp / user.xpToNextLevel) * 100;
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col overflow-hidden">
|
||||
{/* 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">
|
||||
{/* Profile Card */}
|
||||
<div className="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="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="flex h-full w-full items-center justify-center overflow-hidden rounded-full bg-indigo-900">
|
||||
<Image
|
||||
src="/placeholder.svg?height=120&width=120"
|
||||
alt="Profile"
|
||||
width={120}
|
||||
height={120}
|
||||
className="object-cover"
|
||||
/>
|
||||
<main className="mx-auto w-full max-w-4xl flex-1 px-4 py-8">
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{/* Profile Card */}
|
||||
<div className="w-full md:col-span-1">
|
||||
<div className="container-bg sticky top-4 flex w-full flex-col items-center p-6">
|
||||
<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="flex h-full w-full items-center justify-center overflow-hidden rounded-full bg-indigo-900">
|
||||
<Avatar
|
||||
src={user.image!}
|
||||
fb={user.name!}
|
||||
className="size-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-4 w-full truncate overflow-hidden text-center text-2xl font-bold text-white">
|
||||
{user.name}
|
||||
</h1>
|
||||
|
||||
<p className="text-sm text-indigo-200">
|
||||
Member since{" "}
|
||||
{new Date(user?.joinedAt!)?.toLocaleDateString("en-EN", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
})}
|
||||
</p>
|
||||
|
||||
{/* XP Progress */}
|
||||
<div className="mt-6 w-full">
|
||||
{/* <div className="mb-1 flex justify-between text-xs text-indigo-200">
|
||||
<span>XP: {user.xp.toLocaleString()}</span>
|
||||
<span>{user.xpToNextLevel.toLocaleString()}</span>
|
||||
</div> */}
|
||||
<div className="h-3 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-pink-500 to-yellow-400"
|
||||
style={{ width: `${xpProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="mt-1 text-center text-xs text-indigo-200">
|
||||
{Math.floor(user.xpToNextLevel - user.xp).toLocaleString()} XP
|
||||
to level {user.level + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-6 grid w-full grid-cols-2 gap-2">
|
||||
<div className="rounded-lg bg-white/5 p-2 text-center">
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{user.gamesPlayed}
|
||||
</p>
|
||||
<p className="text-xs text-indigo-200">Games</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white/5 p-2 text-center">
|
||||
<p className="text-2xl font-bold text-white">{user.gamesWon}</p>
|
||||
<p className="text-xs text-indigo-200">Wins</p>
|
||||
</div>
|
||||
<div className="col-span-2 rounded-lg bg-white/5 p-2 text-center">
|
||||
<p className="text-2xl font-bold text-white">{user.winRate}%</p>
|
||||
<p className="text-xs text-indigo-200">Win Rate</p>
|
||||
</div>
|
||||
</div>
|
||||
{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`}>
|
||||
Edit Profile
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Achievements and Recent Games */}
|
||||
<div className="space-y-6 md:col-span-2">
|
||||
{/* Achievements */}
|
||||
<div className="container-bg p-6">
|
||||
<div className="mb-4 flex items-center">
|
||||
<Award className="mr-2 h-6 w-6 text-yellow-400" />
|
||||
<h2 className="text-xl font-bold text-white">Achievements</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{user.badges.map((badge, index) => (
|
||||
<div key={index} className="container-bg flex items-center p-3">
|
||||
<div className={`${badge.color} mr-3`}>
|
||||
<badge.icon size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-white">{badge.name}</p>
|
||||
</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">
|
||||
{user.level}
|
||||
))}
|
||||
|
||||
<div className="flex items-center rounded-xl border border-dashed border-white/10 bg-white/5 p-3">
|
||||
<div className="mr-3 text-indigo-300/50">
|
||||
<Medal size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-indigo-300/50">
|
||||
Locked Achievement
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-4 text-2xl font-bold text-white">
|
||||
{user.username}
|
||||
</h1>
|
||||
<p className="text-sm text-indigo-200">
|
||||
Member since {user.joinDate}
|
||||
</p>
|
||||
|
||||
{/* XP Progress */}
|
||||
<div className="mt-6 w-full">
|
||||
<div className="mb-1 flex justify-between text-xs text-indigo-200">
|
||||
<span>XP: {user.xp.toLocaleString()}</span>
|
||||
<span>{user.xpToNextLevel.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="h-3 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-pink-500 to-yellow-400"
|
||||
style={{ width: `${xpProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="mt-1 text-center text-xs text-indigo-200">
|
||||
{Math.floor(user.xpToNextLevel - user.xp).toLocaleString()} XP
|
||||
to level {user.level + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-6 grid w-full grid-cols-3 gap-2">
|
||||
<div className="rounded-lg bg-white/5 p-2 text-center">
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{user.gamesPlayed}
|
||||
</p>
|
||||
<p className="text-xs text-indigo-200">Games</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white/5 p-2 text-center">
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{user.gamesWon}
|
||||
</p>
|
||||
<p className="text-xs text-indigo-200">Wins</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white/5 p-2 text-center">
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{user.winRate}%
|
||||
</p>
|
||||
<p className="text-xs text-indigo-200">Win Rate</p>
|
||||
</div>
|
||||
</div>
|
||||
{session ? (
|
||||
<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
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Achievements and Recent Games */}
|
||||
<div className="space-y-6 md:col-span-2">
|
||||
{/* 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="mb-4 flex items-center">
|
||||
<Award className="mr-2 h-6 w-6 text-yellow-400" />
|
||||
<h2 className="text-xl font-bold text-white">Achievements</h2>
|
||||
</div>
|
||||
{/* Stats Overview */}
|
||||
<div className="container-bg p-6">
|
||||
<div className="mb-4 flex items-center">
|
||||
<BarChart3 className="mr-2 h-6 w-6 text-blue-400" />
|
||||
<h2 className="text-xl font-bold text-white">Stats Overview</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{user.badges.map((badge, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center rounded-xl border border-white/10 bg-white/5 p-3"
|
||||
>
|
||||
<div className={`${badge.color} mr-3`}>
|
||||
<badge.icon size={24} />
|
||||
<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">
|
||||
<h3 className="mb-2 text-sm text-indigo-200">Top Categories</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-white">Science</span>
|
||||
<span className="text-indigo-200">92%</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-white">{badge.name}</p>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-green-500"
|
||||
style={{ width: "92%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex items-center rounded-xl border border-dashed border-white/10 bg-white/5 p-3">
|
||||
<div className="mr-3 text-indigo-300/50">
|
||||
<Medal size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-indigo-300/50">
|
||||
Locked Achievement
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-white">History</span>
|
||||
<span className="text-indigo-200">78%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-500"
|
||||
style={{ width: "78%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-white">Movies</span>
|
||||
<span className="text-indigo-200">65%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-purple-500"
|
||||
style={{ width: "65%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<h3 className="mb-2 text-sm text-indigo-200">Response Time</h3>
|
||||
<div className="flex h-24 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-white">3.2s</p>
|
||||
<p className="text-xs text-indigo-200">
|
||||
Average response time
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-green-400">
|
||||
Faster than 75% of players
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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="mb-4 flex items-center">
|
||||
<BarChart3 className="mr-2 h-6 w-6 text-blue-400" />
|
||||
<h2 className="text-xl font-bold text-white">Stats Overview</h2>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h3 className="mb-2 text-sm text-indigo-200">
|
||||
Top Categories
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-white">Science</span>
|
||||
<span className="text-indigo-200">92%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-green-500"
|
||||
style={{ width: "92%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-white">History</span>
|
||||
<span className="text-indigo-200">78%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-500"
|
||||
style={{ width: "78%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex justify-between text-xs">
|
||||
<span className="text-white">Movies</span>
|
||||
<span className="text-indigo-200">65%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-indigo-900/50">
|
||||
<div
|
||||
className="h-full rounded-full bg-purple-500"
|
||||
style={{ width: "65%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<h3 className="mb-2 text-sm text-indigo-200">
|
||||
Response Time
|
||||
</h3>
|
||||
<div className="flex h-24 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-white">3.2s</p>
|
||||
<p className="text-xs text-indigo-200">
|
||||
Average response time
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-green-400">
|
||||
Faster than 75% of players
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Recent Games */}
|
||||
<div className="container-bg p-6">
|
||||
<div className="mb-4 flex items-center">
|
||||
<Clock className="mr-2 h-6 w-6 text-green-400" />
|
||||
<h2 className="text-xl font-bold text-white">Recent Games</h2>
|
||||
</div>
|
||||
|
||||
{/* 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="mb-4 flex items-center">
|
||||
<Clock className="mr-2 h-6 w-6 text-green-400" />
|
||||
<h2 className="text-xl font-bold text-white">Recent Games</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{user.recentGames.map((game) => (
|
||||
<div
|
||||
key={game.id}
|
||||
className="flex items-center rounded-xl border border-white/10 bg-white/5 p-4"
|
||||
>
|
||||
<div className="mr-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-indigo-800/50">
|
||||
{game.position === "1st" ? (
|
||||
<Trophy className="h-6 w-6 text-yellow-400" />
|
||||
) : game.position === "2nd" ? (
|
||||
<Medal className="h-6 w-6 text-gray-300" />
|
||||
) : (
|
||||
<Medal className="h-6 w-6 text-amber-700" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between">
|
||||
<h3 className="font-medium text-white">{game.topic}</h3>
|
||||
<span className="text-sm text-indigo-200">
|
||||
{game.date}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex justify-between">
|
||||
<span className="text-sm text-indigo-300">
|
||||
Position:{" "}
|
||||
<span className="text-white">{game.position}</span>
|
||||
</span>
|
||||
<span className="text-sm text-indigo-300">
|
||||
Score:{" "}
|
||||
<span className="text-white">{game.score}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mt-2 w-full text-indigo-200 hover:bg-white/10 hover:text-white"
|
||||
<div className="space-y-3">
|
||||
{user.recentGames.map((game) => (
|
||||
<div
|
||||
key={game.id}
|
||||
className="flex items-center rounded-xl border border-white/10 bg-white/5 p-4"
|
||||
>
|
||||
View All Games
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mr-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-indigo-800/50">
|
||||
{game.position === "1st" ? (
|
||||
<Trophy className="h-6 w-6 text-yellow-400" />
|
||||
) : game.position === "2nd" ? (
|
||||
<Medal className="h-6 w-6 text-gray-300" />
|
||||
) : (
|
||||
<Medal className="h-6 w-6 text-amber-700" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between">
|
||||
<h3 className="font-medium text-white">{game.topic}</h3>
|
||||
<span className="text-sm text-indigo-200">
|
||||
{game.date}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex justify-between">
|
||||
<span className="text-sm text-indigo-300">
|
||||
Position:{" "}
|
||||
<span className="text-white">{game.position}</span>
|
||||
</span>
|
||||
<span className="text-sm text-indigo-300">
|
||||
Score: <span className="text-white">{game.score}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mt-2 w-full text-indigo-200 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
View All Games
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -27,8 +26,10 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { api } from "@/trpc/react";
|
||||
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 [loading, setLoading] = React.useState(false);
|
||||
const { mutateAsync } = api.lobby.create.useMutation();
|
||||
@ -51,9 +52,23 @@ function CreateLobbyDialog({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<DialogTitle>Create a Lobby</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
@ -67,6 +82,7 @@ function CreateLobbyDialog({ children }: { children: React.ReactNode }) {
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="placeholder:text-white/50"
|
||||
placeholder="What is you lobbies name?"
|
||||
{...field}
|
||||
/>
|
||||
|
||||
35
src/app/_components/lobby-card.tsx
Normal file
35
src/app/_components/lobby-card.tsx
Normal 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;
|
||||
@ -8,7 +8,11 @@ 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 } 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({
|
||||
session,
|
||||
@ -20,6 +24,7 @@ function LobbyPage({
|
||||
initialMembers: Array<{ leader: boolean } & PublicUser>;
|
||||
}) {
|
||||
const [members, setMembers] = React.useState(initialMembers);
|
||||
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;
|
||||
@ -27,6 +32,13 @@ function LobbyPage({
|
||||
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;
|
||||
@ -36,17 +48,26 @@ function LobbyPage({
|
||||
}, [socket]);
|
||||
|
||||
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>
|
||||
<ul>
|
||||
<ul className="space-y-2">
|
||||
{members?.map((member, idx) => (
|
||||
<li key={idx}>
|
||||
<UserCard name={member.name!} image={member.image!}>
|
||||
{member?.leader && <label>Leader</label>}
|
||||
<UserCard
|
||||
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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{JSON.stringify(memberPresence)}
|
||||
{isOwner && <DeleteLobbyDialog lobbyId={lobby.id} />}
|
||||
{!isOwner && (
|
||||
<LobbyMembershipDialog lobbyId={lobby.id} join={!isJoined} />
|
||||
|
||||
@ -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 React from "react";
|
||||
import {
|
||||
@ -14,31 +8,52 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
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({
|
||||
children,
|
||||
user,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
user: User;
|
||||
}) {
|
||||
function UserPopover({ user }: { user: User }) {
|
||||
const dropdownItemClassName =
|
||||
"focus:bg-border/30 focus:text-white flex items-center gap-1 ";
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel className="font-bold">{user.name}</DropdownMenuLabel>
|
||||
<DropdownMenuTrigger>
|
||||
<Avatar
|
||||
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 />
|
||||
<DropdownMenuItem>
|
||||
<Link href={`/profile/${user.id}`}>View Profile</Link>
|
||||
<DropdownMenuItem className={dropdownItemClassName} asChild>
|
||||
<Link href={`/profile/${user.id}`}>
|
||||
<Eye className="size-4 text-white" />
|
||||
View Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href={`/profile/${user.id}/edit`}>Edit Profile</Link>
|
||||
<DropdownMenuItem className={dropdownItemClassName} asChild>
|
||||
<Link href={`/profile/${user.id}/edit`}>
|
||||
<Edit className="size-4 text-white" />
|
||||
Edit Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="bg-destructive text-destructive-foreground hover:bg-destructive/75 hover:text-destructive-foreground">
|
||||
<Link href="/api/auth/signout">Log out</Link>
|
||||
<DropdownMenuItem
|
||||
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>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
39
src/components/footer.tsx
Normal file
39
src/components/footer.tsx
Normal 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;
|
||||
@ -13,14 +13,14 @@ export const Icons = {
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
|
||||
@ -5,13 +5,31 @@ 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 }: NavLink) {
|
||||
function NavLink({
|
||||
label,
|
||||
path,
|
||||
className,
|
||||
isLast,
|
||||
}: NavLink & { className?: string; isLast?: boolean }) {
|
||||
const active = usePathname() === path;
|
||||
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}>
|
||||
<span>{label}</span>
|
||||
<span
|
||||
className={"transition-transform duration-150 group-hover:scale-105"}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React from "react";
|
||||
import { auth } from "@/server/auth";
|
||||
import Avatar from "./avatar";
|
||||
import { Button } from "./ui/button";
|
||||
import Link from "next/link";
|
||||
import { Home } from "lucide-react";
|
||||
import UserPopover from "@/app/_components/user-popover";
|
||||
import { appConfig } from "@/app.config";
|
||||
import NavLink from "./nav-link";
|
||||
@ -12,24 +10,21 @@ async function Navbar() {
|
||||
const session = await auth();
|
||||
|
||||
return (
|
||||
<nav className="absolute top-0 left-0 z-50 flex h-max w-full items-center justify-center p-4">
|
||||
<div className="bg-background flex w-max items-center gap-2 rounded-full border-2">
|
||||
<menu className="flex items-center">
|
||||
<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">
|
||||
{appConfig.navigation.map((navLink, idx) => (
|
||||
<li key={idx}>
|
||||
<NavLink {...navLink} />
|
||||
<NavLink
|
||||
isLast={idx === appConfig.navigation.length - 1}
|
||||
{...navLink}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</menu>
|
||||
{/* Login Button or Profile Avatar */}
|
||||
{session ? (
|
||||
<UserPopover user={session.user}>
|
||||
<Avatar
|
||||
src={session.user.image!}
|
||||
fb={session.user.name!}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</UserPopover>
|
||||
<UserPopover user={session.user} />
|
||||
) : (
|
||||
<Button asChild className="bg-red-800">
|
||||
<Link href="/api/auth/signin">Sign In</Link>
|
||||
|
||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal 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 }
|
||||
@ -21,14 +21,14 @@ 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 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: {
|
||||
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",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
xxl: "h-16 w-full",
|
||||
xxl: "h-16 px-8 text-lg rounded-2xl has-[>svg]:px-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
@ -17,7 +17,7 @@ function DropdownMenuPortal({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
@ -28,7 +28,7 @@ function DropdownMenuTrigger({
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
@ -43,12 +43,12 @@ function DropdownMenuContent({
|
||||
sideOffset={sideOffset}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
@ -56,7 +56,7 @@ function DropdownMenuGroup({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
@ -65,8 +65,8 @@ function DropdownMenuItem({
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@ -75,11 +75,11 @@ function DropdownMenuItem({
|
||||
data-variant={variant}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -140,7 +140,7 @@ function DropdownMenuRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
@ -148,7 +148,7 @@ function DropdownMenuLabel({
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
@ -156,11 +156,11 @@ function DropdownMenuLabel({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
@ -173,7 +173,7 @@ function DropdownMenuSeparator({
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
@ -185,17 +185,17 @@ function DropdownMenuShortcut({
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: 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({
|
||||
@ -204,7 +204,7 @@ function DropdownMenuSubTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
@ -212,14 +212,14 @@ function DropdownMenuSubTrigger({
|
||||
data-inset={inset}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
@ -231,11 +231,11 @@ function DropdownMenuSubContent({
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -254,4 +254,4 @@ export {
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,21 +1,39 @@
|
||||
import React from "react";
|
||||
import Avatar from "./avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
function UserCard({
|
||||
name,
|
||||
image,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
name: string;
|
||||
image: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Avatar src={image} fb={name} />
|
||||
<div>
|
||||
<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} />
|
||||
<h4 className="font-meidum text-xl">{name}</h4>
|
||||
</div>
|
||||
{children}
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant={"ghost"}
|
||||
className="hover:bg-border/10 hover:text-white"
|
||||
>
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
@ -44,6 +44,7 @@ export const lobbyRouter = createTRPCRouter({
|
||||
image: true,
|
||||
name: true,
|
||||
id: true,
|
||||
joinedAt: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
@ -53,6 +54,7 @@ export const lobbyRouter = createTRPCRouter({
|
||||
image: true,
|
||||
name: true,
|
||||
id: true,
|
||||
joinedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
users,
|
||||
verificationTokens,
|
||||
} 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`
|
||||
@ -20,17 +21,19 @@ declare module "next-auth" {
|
||||
interface Session extends DefaultSession {
|
||||
user: {
|
||||
id: string;
|
||||
joinedAt: Date;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
// interface User {
|
||||
// // ...other properties
|
||||
// // role: UserRole;
|
||||
// }
|
||||
interface User {
|
||||
joinedAt: Date;
|
||||
// ...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.
|
||||
@ -55,7 +58,7 @@ export const authConfig = {
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
verificationTokensTable: verificationTokens,
|
||||
}),
|
||||
}) as Adapter,
|
||||
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
@ -63,6 +66,7 @@ export const authConfig = {
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
joinedAt: user.joinedAt,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@ -17,6 +17,7 @@ export const lobbies = createTable("lobby", (d) => ({
|
||||
.notNull()
|
||||
.$defaultFn(() => createId()),
|
||||
name: d.varchar({ length: 255 }),
|
||||
maxMembers: d.integer().notNull().default(0),
|
||||
|
||||
createdById: d
|
||||
.varchar({ length: 255 })
|
||||
@ -82,6 +83,10 @@ export const users = createTable("user", (d) => ({
|
||||
})
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
image: d.varchar({ length: 255 }),
|
||||
joinedAt: d
|
||||
.timestamp("joined_at", { withTimezone: true })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
}));
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export const JOIN_LOBBY_EVENT = "joinLobby";
|
||||
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";
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
LOBBY_USER_PRESENCE_EVENT,
|
||||
JOIN_LOBBY_EVENT,
|
||||
LEAVE_LOBBY_EVENT,
|
||||
LOBBY_USER_PRESENCE_UPDATE_EVENT,
|
||||
} from "./event-const";
|
||||
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) => {
|
||||
io.on("connection", (socket) => {
|
||||
log("New client connected:", socket.id);
|
||||
@ -20,6 +23,24 @@ export const initSocketEvents = (io: SocketIOServer) => {
|
||||
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}`,
|
||||
);
|
||||
|
||||
@ -19,7 +19,7 @@ const createSocketServer = () => {
|
||||
if (!global.io) {
|
||||
global.io = new Server(server, {
|
||||
cors: {
|
||||
origin,
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
|
||||
@ -113,11 +113,22 @@
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user