Large patch

This commit is contained in:
Ralex91
2024-02-04 17:53:45 +01:00
parent 8c57670742
commit 1e8bd44813
23 changed files with 402 additions and 221 deletions

View File

@@ -0,0 +1,47 @@
import Image from "next/image"
import { usePlayerContext } from "@/context/player"
import Form from "@/components/Form"
import Button from "@/components/Button"
import Input from "@/components/Input"
import { useEffect, useState } from "react"
import { socket } from "@/context/socket"
import logo from "@/assets/logo.svg"
import toast from "react-hot-toast"
export default function ManagerPassword() {
const [loading, setLoading] = useState(false)
const [password, setPassword] = useState("")
const handleCreate = () => {
socket.emit("manager:createRoom", password)
}
useEffect(() => {
socket.on("game:errorMessage", (message) => {
toast.error(message)
})
return () => {
socket.off("game:errorMessage")
}
}, [])
return (
<section className="relative flex min-h-screen flex-col items-center justify-center">
<div className="absolute h-full w-full overflow-hidden">
<div className="absolute -left-[15vmin] -top-[15vmin] min-h-[75vmin] min-w-[75vmin] rounded-full bg-primary/15"></div>
<div className="absolute -bottom-[15vmin] -right-[15vmin] min-h-[75vmin] min-w-[75vmin] rotate-45 bg-primary/15"></div>
</div>
<Image src={logo} className="mb-6 h-32" alt="logo" />
<Form>
<Input
onChange={(e) => setPassword(e.target.value)}
placeholder="Manager password"
/>
<Button onClick={() => handleCreate()}>Submit</Button>
</Form>
</section>
)
}

View File

@@ -1,4 +1,3 @@
import clsx from "clsx"
import { Toaster as ToasterRaw, ToastBar } from "react-hot-toast"
const Toaster = () => (

View File

@@ -7,7 +7,6 @@ import { socket } from "@/context/socket"
export default function Room() {
const { player, dispatch } = usePlayerContext()
const [loading, setLoading] = useState(false)
const [roomId, setRoomId] = useState("")
const handleLogin = () => {
@@ -25,20 +24,12 @@ export default function Room() {
}, [])
return (
<>
{loading && (
<div className="absolute z-30 flex h-full w-full items-center justify-center bg-black/40">
<Loader />
</div>
)}
<Form>
<Input
onChange={(e) => setRoomId(e.target.value)}
placeholder="PIN Code here"
/>
<Button onClick={() => handleLogin()}>Submit</Button>
</Form>
</>
<Form>
<Input
onChange={(e) => setRoomId(e.target.value)}
placeholder="PIN Code here"
/>
<Button onClick={() => handleLogin()}>Submit</Button>
</Form>
)
}

View File

@@ -9,7 +9,6 @@ import { useRouter } from "next/router"
export default function Username() {
const { socket } = useSocketContext()
const { player, dispatch } = usePlayerContext()
const [loading, setLoading] = useState(false)
const router = useRouter()
const [username, setUsername] = useState("")
@@ -33,20 +32,12 @@ export default function Username() {
}, [username])
return (
<>
{loading && (
<div className="absolute z-30 flex h-full w-full items-center justify-center bg-black/40">
<Loader />
</div>
)}
<Form>
<Input
onChange={(e) => setUsername(e.target.value)}
placeholder="Usernname here"
/>
<Button onClick={() => handleJoin()}>Submit</Button>
</Form>
</>
<Form>
<Input
onChange={(e) => setUsername(e.target.value)}
placeholder="Usernname here"
/>
<Button onClick={() => handleJoin()}>Submit</Button>
</Form>
)
}

View File

@@ -1,6 +1,5 @@
import AnswerButton from "../../AnswerButton"
import { useSocketContext } from "@/context/socket"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import clsx from "clsx"
import { ANSWERS_COLORS, ANSWERS_ICONS } from "@/constants"
@@ -66,7 +65,9 @@ export default function Answers({
{question}
</h2>
{/*<Image src={image} className="h-60 w-auto rounded-md" />*/}
{!!image && !responses && (
<img src={image} className="h-48 max-h-60 w-auto rounded-md" />
)}
{responses && (
<div

View File

@@ -0,0 +1,106 @@
import Loader from "@/components/Loader"
import clsx from "clsx"
import { useEffect, useState } from "react"
export default function Podium({ data: { subject, top } }) {
const [apparition, setApparition] = useState(0)
useEffect(() => {
if (top.length < 3) {
setApparition(4)
}
const interval = setInterval(() => {
if (apparition > 4) {
clearInterval(interval)
return
}
setApparition((value) => value + 1)
}, 2000)
return () => clearInterval(interval)
}, [])
return (
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-between">
<h2 className="anim-show text-center text-3xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
{subject}
</h2>
<div
className={`grid max-w-[500px] flex-1 grid-cols-${top.length} w-full items-end justify-center justify-self-end overflow-x-visible`}
>
{top[1] && (
<div
className={clsx(
"z-20 flex h-[50%] w-full flex-col items-center justify-center gap-3 opacity-0",
{ "opacity-100": apparition >= 2 },
)}
>
<p className="overflow-visible whitespace-nowrap text-center text-2xl font-bold text-white drop-shadow-lg">
{top[1].username}
</p>
<div className="flex h-full w-full flex-col items-center gap-4 rounded-t-md bg-primary pt-6 text-center shadow-2xl">
<p className="flex aspect-square h-14 items-center justify-center rounded-full border-4 border-zinc-400 bg-zinc-500 text-3xl font-bold text-white drop-shadow-lg">
<span className="drop-shadow-md">2</span>
</p>
<p className="text-2xl font-bold text-white drop-shadow-lg">
{top[1].points}
</p>
</div>
</div>
)}
<div
className={clsx(
"z-30 flex h-[60%] w-full flex-col items-center gap-3 opacity-0",
{
"opacity-100": apparition >= 3,
},
)}
>
<p
className={clsx(
"overflow-visible whitespace-nowrap text-center text-2xl font-bold text-white opacity-0 drop-shadow-lg",
{ "opacity-100": apparition >= 4 },
)}
>
{top[0].username}
</p>
<div className="flex h-full w-full flex-col items-center gap-4 rounded-t-md bg-primary pt-6 text-center shadow-2xl">
<p className="flex aspect-square h-14 items-center justify-center rounded-full border-4 border-amber-400 bg-amber-300 text-3xl font-bold text-white drop-shadow-lg">
<span className="drop-shadow-md">1</span>
</p>
<p className="text-2xl font-bold text-white drop-shadow-lg">
{top[0].points}
</p>
</div>
</div>
{top[2] && (
<div
className={clsx(
"z-10 flex h-[40%] w-full flex-col items-center gap-3 opacity-0",
{
"opacity-100": apparition >= 1,
},
)}
>
<p className="overflow-visible whitespace-nowrap text-center text-2xl font-bold text-white drop-shadow-lg">
{top[2].username}
</p>
<div className="flex h-full w-full flex-col items-center gap-4 rounded-t-md bg-primary pt-6 text-center shadow-2xl">
<p className="flex aspect-square h-14 items-center justify-center rounded-full border-4 border-amber-800 bg-amber-700 text-3xl font-bold text-white drop-shadow-lg">
<span className="drop-shadow-md">3</span>
</p>
<p className="text-2xl font-bold text-white drop-shadow-lg">
{top[2].points}
</p>
</div>
</div>
)}
</div>
</section>
)
}

View File

@@ -1,19 +1,23 @@
import { useRef } from "react"
export default function Question({ data: { question } }) {
export default function Question({ data: { question, image, cooldown } }) {
const barRef = useRef(null)
return (
<section className="relative mx-auto flex h-full w-full max-w-7xl flex-1 flex-col items-center px-4">
<div className="flex flex-1 items-center">
<div className="flex flex-1 flex-col items-center justify-center gap-5">
<h2 className="anim-show text-center text-3xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
{question}
</h2>
{!!image && (
<img src={image} className="h-48 max-h-60 w-auto rounded-md" />
)}
</div>
<div
ref={barRef}
className="mb-20 h-4 self-start justify-self-end rounded-full bg-primary"
style={{ animation: `progressBar ${6}s linear forwards` }}
style={{ animation: `progressBar ${cooldown}s linear forwards` }}
></div>
</section>
)

View File

@@ -10,12 +10,17 @@ export default function Room({ data: { text, inviteCode } }) {
setPlayerList([...playerList, player])
})
socket.on("manager:removePlayer", (playerId) => {
setPlayerList(playerList.filter((p) => p.id !== playerId))
})
socket.on("manager:playerKicked", (playerId) => {
setPlayerList(playerList.filter((p) => p.id !== playerId))
})
return () => {
socket.off("manager:newPlayer")
socket.off("manager:removePlayer")
socket.off("manager:playerKicked")
}
}, [playerList])

View File

@@ -0,0 +1,39 @@
export default function Pentagon({ className, fill, stroke }) {
return (
<svg
fill={fill}
height="800px"
width="800px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="-40.96 -40.96 593.93 593.93"
transform="rotate(180)"
stroke={fill}
stroke-width="0.005120100000000001"
>
<g stroke-width="0" />
<g
strokeLinecap="round"
strokeLinejoin="round"
stroke={stroke}
strokeWidth="71.6814"
>
<g>
<g>
<path d="M507.804,200.28L262.471,12.866c-3.84-2.923-9.131-2.923-12.949,0L4.188,200.28c-3.605,2.773-5.077,7.531-3.648,11.84 l93.717,281.92c1.451,4.373,5.525,7.296,10.133,7.296h303.253c4.587,0,8.683-2.944,10.133-7.296l93.717-281.92 C512.882,207.789,511.41,203.053,507.804,200.28z" />{" "}
</g>
</g>
</g>
<g>
<g>
<g>
<path d="M507.804,200.28L262.471,12.866c-3.84-2.923-9.131-2.923-12.949,0L4.188,200.28c-3.605,2.773-5.077,7.531-3.648,11.84 l93.717,281.92c1.451,4.373,5.525,7.296,10.133,7.296h303.253c4.587,0,8.683-2.944,10.133-7.296l93.717-281.92 C512.882,207.789,511.41,203.053,507.804,200.28z" />{" "}
</g>
</g>
</g>
</svg>
)
}

View File

@@ -1,15 +1,53 @@
import Answers from "@/components/game/states/Answers"
import Leaderboard from "@/components/game/states/Leaderboard"
import Prepared from "@/components/game/states/Prepared"
import Question from "@/components/game/states/Question"
import Result from "@/components/game/states/Result"
import Room from "./components/game/states/Room"
import Podium from "./components/game/states/Podium"
import Wait from "@/components/game/states/Wait"
import Start from "@/components/game/states/Start"
import Circle from "@/components/icons/Circle"
import Triangle from "@/components/icons/Triangle"
import Square from "@/components/icons/Square"
import Rhombus from "@/components/icons/Rhombus"
export const WEBSOCKET_URL = "http://localhost:5157" ///"https://rahoot.ralex.app"
export const WEBSOCKET_URL = "http://localhost:5157"
export const ANSWERS_COLORS = [
"bg-red-500",
"bg-blue-500",
"bg-yellow-500",
"bg-green-500",
]
"bg-red-500",
"bg-blue-500",
"bg-yellow-500",
"bg-green-500",
]
export const ANSWERS_ICONS = [Triangle, Rhombus, Circle, Square]
export const GAME_STATES = {
status: {
name: "WAIT",
data: { text: "Waiting for the players" },
},
question: {
current: 1,
total: null,
},
}
export const GAME_STATE_COMPONENTS = {
SELECT_ANSWER: Answers,
SHOW_QUESTION: Question,
WAIT: Wait,
SHOW_START: Start,
SHOW_RESULT: Result,
SHOW_PREPARED: Prepared,
}
export const GAME_STATE_COMPONENTS_MANAGER = {
...GAME_STATE_COMPONENTS,
SHOW_ROOM: Room,
SHOW_RESPONSES: Answers,
SHOW_LEADERBOARD: Leaderboard,
FINISH: Podium,
}

View File

@@ -1,5 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default function handler(req, res) {
res.status(200).json({ name: "John Doe" })
}

View File

@@ -1,30 +1,16 @@
import GameWrapper from "@/components/game/GameWrapper"
import Answers from "@/components/game/states/Answers"
import Leaderboard from "@/components/game/states/Leaderboard"
import Prepared from "@/components/game/states/Prepared"
import Question from "@/components/game/states/Question"
import Result from "@/components/game/states/Result"
import Wait from "@/components/game/states/Wait"
import Start from "@/components/game/states/Start"
import { GAME_STATES, GAME_STATE_COMPONENTS } from "@/constants"
import { usePlayerContext } from "@/context/player"
import { useSocketContext } from "@/context/socket"
import { useRouter } from "next/router"
import { createElement, useEffect, useState } from "react"
const gameStateComponent = {
SELECT_ANSWER: Answers,
SHOW_QUESTION: Question,
WAIT: Wait,
SHOW_START: Start,
SHOW_RESULT: Result,
SHOW_PREPARED: Prepared,
}
import toast from "react-hot-toast"
export default function Game() {
const router = useRouter()
const { socket } = useSocketContext()
const { player } = usePlayerContext()
const { player, dispatch } = usePlayerContext()
useEffect(() => {
if (!player) {
@@ -32,16 +18,7 @@ export default function Game() {
}
}, [])
const [state, setState] = useState({
status: {
name: "WAIT",
data: { text: "Waiting for the players" },
},
question: {
current: 1,
total: null,
},
})
const [state, setState] = useState(GAME_STATES)
useEffect(() => {
socket.on("game:status", (status) => {
@@ -55,15 +32,25 @@ export default function Game() {
})
})
socket.on("game:reset", () => {
router.replace("/")
dispatch({ type: "LOGOUT" })
setState(GAME_STATES)
toast("The game has been reset by the host")
})
return () => {
socket.off("game:status")
socket.off("game:reset")
}
}, [state])
return (
<GameWrapper>
{gameStateComponent[state.status.name] &&
createElement(gameStateComponent[state.status.name], {
{GAME_STATE_COMPONENTS[state.status.name] &&
createElement(GAME_STATE_COMPONENTS[state.status.name], {
data: state.status.data,
})}
</GameWrapper>

View File

@@ -13,8 +13,6 @@ import { useSocketContext } from "@/context/socket"
import toast from "react-hot-toast"
export default function Home() {
const [loading, setLoading] = useState(false)
const { player, dispatch } = usePlayerContext()
const { socket } = useSocketContext()
@@ -35,12 +33,6 @@ export default function Home() {
<div className="absolute -bottom-[15vmin] -right-[15vmin] min-h-[75vmin] min-w-[75vmin] rotate-45 bg-primary/15"></div>
</div>
{loading && (
<div className="absolute z-30 flex h-full w-full items-center justify-center bg-black/40">
<Loader />
</div>
)}
<Image src={logo} className="mb-6 h-32" alt="logo" />
{!player ? <Room /> : <Username />}

View File

@@ -1,41 +1,21 @@
import Button from "@/components/Button"
import GameWrapper from "@/components/game/GameWrapper"
import Answers from "@/components/game/states/Answers"
import Leaderboard from "@/components/game/states/Leaderboard"
import Prepared from "@/components/game/states/Prepared"
import Question from "@/components/game/states/Question"
import Room from "@/components/game/states/Room"
import Start from "@/components/game/states/Start"
import Wait from "@/components/game/states/Wait"
import ManagerPassword from "@/components/ManagerPassword"
import { GAME_STATES, GAME_STATE_COMPONENTS_MANAGER } from "@/constants"
import { usePlayerContext } from "@/context/player"
import { useSocketContext } from "@/context/socket"
import { useRouter } from "next/router"
import { createElement, useEffect, useState } from "react"
const gameStateComponent = {
SHOW_ROOM: Room,
SHOW_START: Start,
SELECT_ANSWER: Answers,
SHOW_QUESTION: Question,
WAIT: Wait,
SHOW_RESPONSES: Answers,
SHOW_LEADERBOARD: Leaderboard,
SHOW_PREPARED: Prepared,
}
export default function Manager() {
const { socket } = useSocketContext()
const [nextText, setNextText] = useState("Start")
const [state, setState] = useState({
created: false,
...GAME_STATES,
status: {
...GAME_STATES.status,
name: "SHOW_ROOM",
data: { text: "Waiting for the players" },
},
question: {
current: 1,
total: null,
},
})
@@ -97,13 +77,13 @@ export default function Manager() {
<>
{!state.created ? (
<div>
<Button onClick={handleCreate}>Create Room</Button>
<ManagerPassword />
</div>
) : (
<>
<GameWrapper textNext={nextText} onNext={handleSkip} manager>
{gameStateComponent[state.status.name] &&
createElement(gameStateComponent[state.status.name], {
{GAME_STATE_COMPONENTS_MANAGER[state.status.name] &&
createElement(GAME_STATE_COMPONENTS_MANAGER[state.status.name], {
data: state.status.data,
})}
</GameWrapper>

View File

@@ -1,63 +0,0 @@
import GameWrapper from "@/components/game/GameWrapper"
import Answers from "@/components/game/states/Answers"
import Leaderboard from "@/components/game/states/Leaderboard"
import Question from "@/components/game/states/Question"
import Wait from "@/components/game/states/Wait"
import { useSocketContext } from "@/context/socket"
import { createElement, useEffect, useState } from "react"
export default function Manager({ children }) {
const { socket } = useSocketContext()
const [gameState, setGameState] = useState({
status: {
name: "PENDING",
},
cooldown: 0,
answers: [],
})
socket.on("game:status", (status) => {
setGameState({
...gameState,
status: status,
cooldown: 0,
})
})
return (
<>
<p className="text-white">{JSON.stringify(gameState)}</p>
<hr></hr>
<div className="flex gap-2">
<button
className="bg-primary p-2"
onClick={() => socket.emit("manager:createRoom")}
>
Create Room
</button>
<button
className="bg-primary p-2"
onClick={() => socket.emit("manager:startGame")}
>
Start Game
</button>
<button
className="bg-primary p-2"
onClick={() => socket.emit("manager:showLeaderboard")}
>
Leaderboard
</button>
<button
className="bg-primary p-2"
onClick={() => socket.emit("manager:nextQuestion")}
>
Next Question
</button>
</div>
</>
)
}