mirror of
https://github.com/randyjc/Rahoot.git
synced 2026-03-14 04:25:35 +01:00
refactor: add typescript & pnpm workspace & docker file
This commit is contained in:
17
packages/web/src/app/(auth)/layout.tsx
Normal file
17
packages/web/src/app/(auth)/layout.tsx
Normal 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
|
||||
43
packages/web/src/app/(auth)/manager/page.tsx
Normal file
43
packages/web/src/app/(auth)/manager/page.tsx
Normal 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} />
|
||||
}
|
||||
38
packages/web/src/app/(auth)/page.tsx
Normal file
38
packages/web/src/app/(auth)/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
78
packages/web/src/app/game/[gameId]/page.tsx
Normal file
78
packages/web/src/app/game/[gameId]/page.tsx
Normal 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>
|
||||
}
|
||||
17
packages/web/src/app/game/layout.tsx
Normal file
17
packages/web/src/app/game/layout.tsx
Normal 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
|
||||
112
packages/web/src/app/game/manager/[gameId]/page.tsx
Normal file
112
packages/web/src/app/game/manager/[gameId]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
194
packages/web/src/app/globals.css
Normal file
194
packages/web/src/app/globals.css
Normal 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);
|
||||
}
|
||||
}
|
||||
29
packages/web/src/app/layout.tsx
Normal file
29
packages/web/src/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
10
packages/web/src/app/socket/route.ts
Normal file
10
packages/web/src/app/socket/route.ts
Normal 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"
|
||||
Reference in New Issue
Block a user