refactor: add typescript & pnpm workspace & docker file

This commit is contained in:
Ralex
2025-10-16 23:12:40 +02:00
parent 8f73241f34
commit edb7146d6d
122 changed files with 7568 additions and 8502 deletions

View File

@@ -0,0 +1,17 @@
"use client"
import { useSocket } from "@rahoot/web/contexts/socketProvider"
import { PropsWithChildren, useEffect } from "react"
const AuthLayout = ({ children }: PropsWithChildren) => {
const { isConnected, connect } = useSocket()
useEffect(() => {
if (!isConnected) {
connect()
}
}, [connect, isConnected])
return children
}
export default AuthLayout

View File

@@ -0,0 +1,43 @@
"use client"
import { QuizzWithId } from "@rahoot/common/types/game"
import ManagerPassword from "@rahoot/web/components/game/create/ManagerPassword"
import SelectQuizz from "@rahoot/web/components/game/create/SelectQuizz"
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
import { useManagerGameStore } from "@rahoot/web/stores/game"
import { useRouter } from "next/navigation"
import { useState } from "react"
export default function Manager() {
const { setStatus } = useManagerGameStore()
const router = useRouter()
const { socket } = useSocket()
const [isAuth, setIsAuth] = useState(false)
const [quizzList, setQuizzList] = useState<QuizzWithId[]>([])
useEvent("manager:quizzList", (quizzList) => {
setIsAuth(true)
setQuizzList(quizzList)
})
useEvent("manager:gameCreated", ({ gameId, inviteCode }) => {
setStatus("SHOW_ROOM", { text: "Waiting for the players", inviteCode })
router.push(`/game/manager/${gameId}`)
})
const handleAuth = (password: string) => {
socket?.emit("manager:auth", password)
}
const handleCreate = (quizzId: string) => {
console.log(quizzId)
socket?.emit("game:create", quizzId)
console.log("create room")
}
if (!isAuth) {
return <ManagerPassword onSubmit={handleAuth} />
}
return <SelectQuizz quizzList={quizzList} onSelect={handleCreate} />
}

View File

@@ -0,0 +1,38 @@
"use client"
import logo from "@rahoot/web/assets/logo.svg"
import Room from "@rahoot/web/components/game/join/Room"
import Username from "@rahoot/web/components/game/join/Username"
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
import { usePlayerStore } from "@rahoot/web/stores/player"
import Image from "next/image"
import { useEffect } from "react"
import toast from "react-hot-toast"
export default function Home() {
const { isConnected, connect } = useSocket()
const { player } = usePlayerStore()
useEffect(() => {
if (!isConnected) {
connect()
}
}, [connect, isConnected])
useEvent("game:errorMessage", (message) => {
toast.error(message)
})
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="bg-primary/15 absolute -top-[15vmin] -left-[15vmin] min-h-[75vmin] min-w-[75vmin] rounded-full"></div>
<div className="bg-primary/15 absolute -right-[15vmin] -bottom-[15vmin] min-h-[75vmin] min-w-[75vmin] rotate-45"></div>
</div>
<Image src={logo} className="mb-6 h-32" alt="logo" />
{!player ? <Room /> : <Username />}
</section>
)
}

View File

@@ -0,0 +1,78 @@
"use client"
import { Status } from "@rahoot/common/types/game/status"
import GameWrapper from "@rahoot/web/components/game/GameWrapper"
import Answers from "@rahoot/web/components/game/states/Answers"
import Prepared from "@rahoot/web/components/game/states/Prepared"
import Question from "@rahoot/web/components/game/states/Question"
import Result from "@rahoot/web/components/game/states/Result"
import Start from "@rahoot/web/components/game/states/Start"
import Wait from "@rahoot/web/components/game/states/Wait"
import { useEvent } from "@rahoot/web/contexts/socketProvider"
import { usePlayerGameStore } from "@rahoot/web/stores/game"
import { usePlayerStore } from "@rahoot/web/stores/player"
import { GAME_STATE_COMPONENTS } from "@rahoot/web/utils/constants"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import toast from "react-hot-toast"
export default function Game() {
const router = useRouter()
const { player, logout } = usePlayerStore()
const { status, setStatus, resetStatus } = usePlayerGameStore()
useEffect(() => {
if (!player) {
router.replace("/")
}
}, [player, router])
useEvent("game:status", ({ name, data }) => {
if (name in GAME_STATE_COMPONENTS) {
setStatus(name, data)
}
})
useEvent("game:reset", () => {
router.replace("/")
logout()
resetStatus()
toast("The game has been reset by the host")
})
let component = null
switch (status.name) {
case Status.WAIT:
component = <Wait data={status.data} />
break
case Status.SHOW_START:
component = <Start data={status.data} />
break
case Status.SHOW_PREPARED:
component = <Prepared data={status.data} />
break
case Status.SHOW_QUESTION:
component = <Question data={status.data} />
break
case Status.SHOW_RESULT:
component = <Result data={status.data} />
break
case Status.SELECT_ANSWER:
component = <Answers data={status.data} />
break
}
return <GameWrapper>{component}</GameWrapper>
}

View File

@@ -0,0 +1,17 @@
"use client"
import { useSocket } from "@rahoot/web/contexts/socketProvider"
import { PropsWithChildren, useEffect } from "react"
const GameLayout = ({ children }: PropsWithChildren) => {
const { isConnected, connect } = useSocket()
useEffect(() => {
if (!isConnected) {
connect()
}
}, [connect, isConnected])
return children
}
export default GameLayout

View File

@@ -0,0 +1,112 @@
"use client"
import { Status } from "@rahoot/common/types/game/status"
import GameWrapper from "@rahoot/web/components/game/GameWrapper"
import Answers from "@rahoot/web/components/game/states/Answers"
import Leaderboard from "@rahoot/web/components/game/states/Leaderboard"
import Podium from "@rahoot/web/components/game/states/Podium"
import Prepared from "@rahoot/web/components/game/states/Prepared"
import Question from "@rahoot/web/components/game/states/Question"
import Responses from "@rahoot/web/components/game/states/Responses"
import Room from "@rahoot/web/components/game/states/Room"
import Start from "@rahoot/web/components/game/states/Start"
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
import { useManagerGameStore } from "@rahoot/web/stores/game"
import { GAME_STATE_COMPONENTS_MANAGER } from "@rahoot/web/utils/constants"
import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
export default function ManagerGame() {
const { socket } = useSocket()
const [nextText, setNextText] = useState("Start")
const { status, setStatus } = useManagerGameStore()
const { gameId }: { gameId?: string } = useParams()
useEvent("game:status", ({ name, data }) => {
if (name in GAME_STATE_COMPONENTS_MANAGER) {
setStatus(name, data)
}
})
useEffect(() => {
if (status.name === "SHOW_START") {
setNextText("Start")
}
}, [status.name])
const handleSkip = () => {
setNextText("Skip")
switch (status.name) {
case Status.SHOW_ROOM:
socket?.emit("manager:startGame", { gameId })
break
case Status.SELECT_ANSWER:
socket?.emit("manager:abortQuiz", { gameId })
break
case Status.SHOW_RESPONSES:
socket?.emit("manager:showLeaderboard", { gameId })
break
case Status.SHOW_LEADERBOARD:
socket?.emit("manager:nextQuestion", { gameId })
break
}
}
let component = null
switch (status.name) {
case Status.SHOW_ROOM:
component = <Room data={status.data} />
break
case Status.SHOW_START:
component = <Start data={status.data} />
break
case Status.SHOW_PREPARED:
component = <Prepared data={status.data} />
break
case Status.SHOW_QUESTION:
component = <Question data={status.data} />
break
case Status.SELECT_ANSWER:
component = <Answers data={status.data} />
break
case Status.SHOW_RESPONSES:
component = <Responses data={status.data} />
break
case Status.SHOW_LEADERBOARD:
component = <Leaderboard data={status.data} />
break
case Status.FINISHED:
component = <Podium data={status.data} />
break
}
return (
<GameWrapper textNext={nextText} onNext={handleSkip} manager>
{component}
</GameWrapper>
)
}

View File

@@ -0,0 +1,194 @@
@import "tailwindcss";
@theme {
--color-primary: #ff9900;
--color-secondary: #1a140b;
}
.btn-shadow {
box-shadow: rgba(0, 0, 0, 0.25) 0px -4px inset;
}
.btn-shadow span {
display: block;
transform: translateY(-2px);
}
.btn-shadow:hover {
box-shadow: rgba(0, 0, 0, 0.25) 0px -2px inset;
}
.btn-shadow:hover span {
transform: translateY(0);
}
.btn-shadow:active {
transform: translateY(1px);
box-shadow: none;
}
.text-outline {
-webkit-text-stroke: 2px rgba(0, 0, 0, 0.25);
}
.shadow-inset {
box-shadow: rgba(0, 0, 0, 0.25) 0px -4px inset;
}
.spotlight {
position: absolute;
height: 200%;
width: 200%;
z-index: 100;
background-image: radial-gradient(
circle,
transparent 180px,
rgba(0, 0, 0, 0.6) 200px
);
opacity: 0;
left: -50%;
top: -50%;
transition: all 0.5s;
animation: spotlightAnim 2.5s ease-in;
}
@keyframes spotlightAnim {
0% {
left: -20%;
top: -20%;
}
30% {
opacity: 100;
top: -80%;
left: -80%;
}
60% {
top: -50%;
left: -20%;
}
80% {
top: -50%;
left: -50%;
}
98% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.anim-show {
animation: show 0.5s ease-out;
}
.anim-timer {
animation: timer 1s ease-out infinite;
}
.anim-quizz {
animation: quizz 0.8s linear;
transform: perspective(1200px) rotateY(-15deg) rotateX(15deg)
translateZ(100px);
box-shadow: 10px 10px 0 rgba(20, 24, 29, 1);
}
.anim-quizz .button {
box-shadow: rgba(0, 0, 0, 0.25) -4px -4px inset;
animation: quizzButton 0.8s ease-out;
}
.anim-balanced {
animation: balanced 0.8s linear infinite;
}
@keyframes balanced {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(-10deg) translateY(-10px);
}
50% {
transform: rotate(0deg) translateY(0px);
}
75% {
transform: rotate(10deg) translateY(-10px);
}
100% {
transform: rotate(0deg);
}
}
@keyframes show {
0% {
transform: scale(0);
}
30% {
transform: scale(0.9);
}
60% {
transform: scale(0.8);
}
80% {
transform: scale(1);
}
}
@keyframes progressBar {
from {
width: 0%;
}
to {
width: 100%;
}
}
@keyframes timer {
0% {
transform: scale(1);
}
30% {
transform: scale(1.4) rotate(-6deg);
}
60% {
transform: scale(0.8) rotate(6deg);
}
80% {
transform: scale(1);
}
}
@keyframes quizz {
0% {
transform: scale(0) perspective(1200px) rotateY(-60deg) rotateX(60deg)
translateZ(100px);
}
60% {
transform: scale(1) perspective(1200px) rotateY(-15deg) rotateX(15deg)
translateZ(100px);
}
80% {
transform: scale(0.8) perspective(1200px) rotateY(-15deg) rotateX(15deg)
translateZ(100px);
}
100% {
transform: scale(1) perspective(1200px) rotateY(-15deg) rotateX(15deg)
translateZ(100px);
}
}
@keyframes quizzButton {
0% {
transform: scale(0);
}
60% {
transform: scale(1);
}
80% {
transform: scale(0.8);
}
100% {
transform: scale(1);
}
}

View File

@@ -0,0 +1,29 @@
import Toaster from "@rahoot/web/components/Toaster"
import { SocketProvider } from "@rahoot/web/contexts/socketProvider"
import type { Metadata } from "next"
import { Montserrat } from "next/font/google"
import { PropsWithChildren } from "react"
import "./globals.css"
const montserrat = Montserrat({
variable: "--font-montserrat",
subsets: ["latin"],
})
export const metadata: Metadata = {
title: "Rahoot !",
icons: "/icon.svg",
}
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en" suppressHydrationWarning={true} data-lt-installed="true">
<body className={`${montserrat.variable} bg-secondary antialiased`}>
<SocketProvider>
<main className="text-base-[8px] flex flex-col">{children}</main>
<Toaster />
</SocketProvider>
</body>
</html>
)
}

View File

@@ -0,0 +1,10 @@
import env from "@rahoot/web/env"
import { NextResponse } from "next/server"
export function GET() {
return NextResponse.json({
url: env.SOCKET_URL,
})
}
export const dynamic = "force-dynamic"