mirror of
https://github.com/randyjc/Rahoot.git
synced 2026-03-13 20:15:35 +01:00
refactor: add typescript & pnpm workspace & docker file
This commit is contained in:
27
packages/web/src/components/AnswerButton.tsx
Normal file
27
packages/web/src/components/AnswerButton.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import clsx from "clsx"
|
||||
import { ButtonHTMLAttributes, ElementType, PropsWithChildren } from "react"
|
||||
|
||||
type Props = PropsWithChildren &
|
||||
ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
icon: ElementType
|
||||
}
|
||||
|
||||
export default function AnswerButton({
|
||||
className,
|
||||
icon: Icon,
|
||||
children,
|
||||
...otherProps
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"shadow-inset flex items-center gap-3 rounded px-4 py-6 text-left",
|
||||
className,
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon className="h-6 w-6" />
|
||||
<span className="drop-shadow-md">{children}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
17
packages/web/src/components/Button.tsx
Normal file
17
packages/web/src/components/Button.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import clsx from "clsx"
|
||||
import { ButtonHTMLAttributes, PropsWithChildren } from "react"
|
||||
|
||||
type Props = ButtonHTMLAttributes<HTMLButtonElement> & PropsWithChildren
|
||||
export default function Button({ children, className, ...otherProps }: Props) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"btn-shadow bg-primary rounded-md p-2 text-lg font-semibold text-white",
|
||||
className,
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<span>{children}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
9
packages/web/src/components/Form.tsx
Normal file
9
packages/web/src/components/Form.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PropsWithChildren } from "react"
|
||||
|
||||
export default function Form({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="z-10 flex w-full max-w-80 flex-col gap-4 rounded-md bg-white p-4 shadow-sm">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
packages/web/src/components/Input.tsx
Normal file
21
packages/web/src/components/Input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
|
||||
type Props = React.InputHTMLAttributes<HTMLInputElement>
|
||||
|
||||
export default function Input({
|
||||
className,
|
||||
type = "text",
|
||||
...otherProps
|
||||
}: Props) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={clsx(
|
||||
"rounded-sm p-2 text-lg font-semibold outline-2 outline-gray-300",
|
||||
className,
|
||||
)}
|
||||
{...otherProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
6
packages/web/src/components/Loader.tsx
Normal file
6
packages/web/src/components/Loader.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import loader from "@rahoot/web/assets/loader.svg"
|
||||
import Image from "next/image"
|
||||
|
||||
export default function Loader() {
|
||||
return <Image alt="loader" src={loader} />
|
||||
}
|
||||
27
packages/web/src/components/Toaster.tsx
Normal file
27
packages/web/src/components/Toaster.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { ToastBar, Toaster as ToasterRaw } from "react-hot-toast"
|
||||
|
||||
const Toaster = () => (
|
||||
<ToasterRaw>
|
||||
{(t) => (
|
||||
<ToastBar
|
||||
toast={t}
|
||||
style={{
|
||||
...t.style,
|
||||
boxShadow: "rgba(0, 0, 0, 0.25) 0px -4px inset",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{({ icon, message }) => (
|
||||
<>
|
||||
{icon}
|
||||
{message}
|
||||
</>
|
||||
)}
|
||||
</ToastBar>
|
||||
)}
|
||||
</ToasterRaw>
|
||||
)
|
||||
|
||||
export default Toaster
|
||||
87
packages/web/src/components/game/GameWrapper.tsx
Normal file
87
packages/web/src/components/game/GameWrapper.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { GameUpdateQuestion } from "@rahoot/common/types/game"
|
||||
import background from "@rahoot/web/assets/background.webp"
|
||||
import Button from "@rahoot/web/components/Button"
|
||||
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
|
||||
import { usePlayerStore } from "@rahoot/web/stores/player"
|
||||
import Image from "next/image"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { PropsWithChildren, useEffect, useState } from "react"
|
||||
|
||||
type Props = PropsWithChildren & {
|
||||
textNext?: string
|
||||
onNext?: () => void
|
||||
manager?: boolean
|
||||
}
|
||||
|
||||
export default function GameWrapper({
|
||||
children,
|
||||
textNext,
|
||||
onNext,
|
||||
manager,
|
||||
}: Props) {
|
||||
const { isConnected, connect } = useSocket()
|
||||
const { player, logout } = usePlayerStore()
|
||||
const router = useRouter()
|
||||
|
||||
const [questionState, setQuestionState] = useState<GameUpdateQuestion>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) {
|
||||
connect()
|
||||
}
|
||||
}, [connect, isConnected])
|
||||
|
||||
useEvent("game:kick", () => {
|
||||
logout()
|
||||
router.replace("/")
|
||||
})
|
||||
|
||||
useEvent("game:updateQuestion", ({ current, total }) => {
|
||||
setQuestionState({
|
||||
current,
|
||||
total,
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<section className="relative flex min-h-screen w-full flex-col justify-between">
|
||||
<div className="fixed top-0 left-0 -z-10 h-full w-full bg-orange-600 opacity-70">
|
||||
<Image
|
||||
className="pointer-events-none h-full w-full object-cover opacity-60"
|
||||
src={background}
|
||||
alt="background"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-between p-4">
|
||||
{questionState && (
|
||||
<div className="shadow-inset flex items-center rounded-md bg-white p-2 px-4 text-lg font-bold text-black">
|
||||
{`${questionState.current} / ${questionState.total}`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{manager && (
|
||||
<Button
|
||||
className="self-end bg-white px-4 !text-black"
|
||||
onClick={onNext}
|
||||
>
|
||||
{textNext}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
{!manager && (
|
||||
<div className="z-50 flex items-center justify-between bg-white px-4 py-2 text-lg font-bold text-white">
|
||||
<p className="text-gray-800">{player?.username}</p>
|
||||
<div className="rounded-sm bg-gray-800 px-3 py-1 text-lg">
|
||||
{player?.points}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
52
packages/web/src/components/game/create/ManagerPassword.tsx
Normal file
52
packages/web/src/components/game/create/ManagerPassword.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import logo from "@rahoot/web/assets/logo.svg"
|
||||
import Button from "@rahoot/web/components/Button"
|
||||
import Form from "@rahoot/web/components/Form"
|
||||
import Input from "@rahoot/web/components/Input"
|
||||
import { useEvent } from "@rahoot/web/contexts/socketProvider"
|
||||
import Image from "next/image"
|
||||
import { KeyboardEvent, useState } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
|
||||
type Props = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onSubmit: (password: string) => void
|
||||
}
|
||||
|
||||
export default function ManagerPassword({ onSubmit }: Props) {
|
||||
const [password, setPassword] = useState("")
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(password)
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
useEvent("manager: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" />
|
||||
|
||||
<Form>
|
||||
<Input
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Manager password"
|
||||
/>
|
||||
<Button onClick={handleSubmit}>Submit</Button>
|
||||
</Form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
79
packages/web/src/components/game/create/SelectQuizz.tsx
Normal file
79
packages/web/src/components/game/create/SelectQuizz.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Quizz } from "@rahoot/common/types/game"
|
||||
import logo from "@rahoot/web/assets/logo.svg"
|
||||
import Button from "@rahoot/web/components/Button"
|
||||
import clsx from "clsx"
|
||||
import Image from "next/image"
|
||||
import { useState } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
|
||||
type QuizzWithId = Quizz & { id: string }
|
||||
|
||||
type Props = {
|
||||
quizzList: QuizzWithId[]
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onSelect: (id: string) => void
|
||||
}
|
||||
|
||||
export default function SelectQuizz({ quizzList, onSelect }: Props) {
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
|
||||
const handleSelect = (id: string) => () => {
|
||||
if (selected === id) {
|
||||
setSelected(null)
|
||||
} else {
|
||||
setSelected(id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!selected) {
|
||||
toast.error("Please select a quizz")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(selected)
|
||||
}
|
||||
|
||||
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" />
|
||||
|
||||
<div className="z-10 flex w-full max-w-md flex-col gap-4 rounded-md bg-white p-4 shadow-sm">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="mb-2 text-2xl font-bold">Select a quizz</h1>
|
||||
<div className="w-full space-y-2">
|
||||
{quizzList.map((quizz) => (
|
||||
<button
|
||||
key={quizz.id}
|
||||
className={clsx(
|
||||
"flex w-full items-center justify-between rounded-md p-3 outline outline-gray-300",
|
||||
{
|
||||
"border-primary outline-primary outline-2":
|
||||
selected === quizz.id,
|
||||
},
|
||||
)}
|
||||
onClick={handleSelect(quizz.id)}
|
||||
>
|
||||
{quizz.subject}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"h-4 w-4 rounded-sm outline-2 outline-gray-300",
|
||||
selected === quizz.id && "bg-primary outline-primary/50",
|
||||
)}
|
||||
></div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleSubmit}>Submit</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
37
packages/web/src/components/game/join/Room.tsx
Normal file
37
packages/web/src/components/game/join/Room.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import Button from "@rahoot/web/components/Button"
|
||||
import Form from "@rahoot/web/components/Form"
|
||||
import Input from "@rahoot/web/components/Input"
|
||||
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
|
||||
import { usePlayerStore } from "@rahoot/web/stores/player"
|
||||
import { KeyboardEvent, useState } from "react"
|
||||
|
||||
export default function Room() {
|
||||
const { socket } = useSocket()
|
||||
const { join } = usePlayerStore()
|
||||
const [invitation, setInvitation] = useState("")
|
||||
|
||||
const handleJoin = () => {
|
||||
socket?.emit("player:join", invitation)
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
handleJoin()
|
||||
}
|
||||
}
|
||||
|
||||
useEvent("game:successRoom", (gameId) => {
|
||||
join(gameId)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Input
|
||||
onChange={(e) => setInvitation(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="PIN Code here"
|
||||
/>
|
||||
<Button onClick={handleJoin}>Submit</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
44
packages/web/src/components/game/join/Username.tsx
Normal file
44
packages/web/src/components/game/join/Username.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import Button from "@rahoot/web/components/Button"
|
||||
import Form from "@rahoot/web/components/Form"
|
||||
import Input from "@rahoot/web/components/Input"
|
||||
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
|
||||
import { usePlayerStore } from "@rahoot/web/stores/player"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { KeyboardEvent, useState } from "react"
|
||||
|
||||
export default function Username() {
|
||||
const { socket } = useSocket()
|
||||
const { player, login } = usePlayerStore()
|
||||
const router = useRouter()
|
||||
const [username, setUsername] = useState("")
|
||||
|
||||
const handleLogin = () => {
|
||||
socket?.emit("player:login", { gameId: player?.gameId, data: { username } })
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
handleLogin()
|
||||
}
|
||||
}
|
||||
|
||||
useEvent("game:successJoin", (gameId) => {
|
||||
login(username)
|
||||
|
||||
router.replace(`/game/${gameId}`)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Input
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Username here"
|
||||
/>
|
||||
<Button onClick={handleLogin}>Submit</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
114
packages/web/src/components/game/states/Answers.tsx
Normal file
114
packages/web/src/components/game/states/Answers.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import { CommonStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import AnswerButton from "@rahoot/web/components/AnswerButton"
|
||||
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
|
||||
import { usePlayerStore } from "@rahoot/web/stores/player"
|
||||
import {
|
||||
ANSWERS_COLORS,
|
||||
ANSWERS_ICONS,
|
||||
SFX_ANSWERS_MUSIC,
|
||||
SFX_ANSWERS_SOUND,
|
||||
} from "@rahoot/web/utils/constants"
|
||||
import clsx from "clsx"
|
||||
import { useParams } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import useSound from "use-sound"
|
||||
|
||||
type Props = {
|
||||
data: CommonStatusDataMap["SELECT_ANSWER"]
|
||||
}
|
||||
|
||||
export default function Answers({
|
||||
data: { question, answers, image, time, totalPlayer },
|
||||
}: Props) {
|
||||
const { gameId }: { gameId?: string } = useParams()
|
||||
const { socket } = useSocket()
|
||||
const { player } = usePlayerStore()
|
||||
|
||||
const [cooldown, setCooldown] = useState(time)
|
||||
const [totalAnswer, setTotalAnswer] = useState(0)
|
||||
|
||||
const [sfxPop] = useSound(SFX_ANSWERS_SOUND, {
|
||||
volume: 0.1,
|
||||
})
|
||||
|
||||
const [playMusic] = useSound(SFX_ANSWERS_MUSIC, {
|
||||
volume: 0.2,
|
||||
interrupt: true,
|
||||
})
|
||||
|
||||
const handleAnswer = (answerKey: number) => () => {
|
||||
if (!player) {
|
||||
return
|
||||
}
|
||||
|
||||
socket?.emit("player:selectedAnswer", {
|
||||
gameId,
|
||||
data: {
|
||||
answerKey,
|
||||
},
|
||||
})
|
||||
sfxPop()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("play music")
|
||||
playMusic()
|
||||
}, [])
|
||||
|
||||
useEvent("game:cooldown", (sec) => {
|
||||
setCooldown(sec)
|
||||
})
|
||||
|
||||
useEvent("game:playerAnswer", (count) => {
|
||||
setTotalAnswer(count)
|
||||
sfxPop()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-1 flex-col justify-between">
|
||||
<div className="mx-auto inline-flex h-full w-full max-w-7xl flex-1 flex-col items-center justify-center gap-5">
|
||||
<h2 className="text-center text-2xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
|
||||
{question}
|
||||
</h2>
|
||||
|
||||
{Boolean(image) && (
|
||||
<img
|
||||
alt={question}
|
||||
src={image}
|
||||
className="h-48 max-h-60 w-auto rounded-md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mx-auto mb-4 flex w-full max-w-7xl justify-between gap-1 px-2 text-lg font-bold text-white md:text-xl">
|
||||
<div className="flex flex-col items-center rounded-full bg-black/40 px-4 text-lg font-bold">
|
||||
<span className="translate-y-1 text-sm">Time</span>
|
||||
<span>{cooldown}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center rounded-full bg-black/40 px-4 text-lg font-bold">
|
||||
<span className="translate-y-1 text-sm">Answers</span>
|
||||
<span>
|
||||
{totalAnswer}/{totalPlayer}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-4 grid w-full max-w-7xl grid-cols-2 gap-1 rounded-full px-2 text-lg font-bold text-white md:text-xl">
|
||||
{answers.map((answer, key) => (
|
||||
<AnswerButton
|
||||
key={key}
|
||||
className={clsx(ANSWERS_COLORS[key])}
|
||||
icon={ANSWERS_ICONS[key]}
|
||||
onClick={handleAnswer(key)}
|
||||
>
|
||||
{answer}
|
||||
</AnswerButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
packages/web/src/components/game/states/Leaderboard.tsx
Normal file
26
packages/web/src/components/game/states/Leaderboard.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ManagerStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
|
||||
type Props = {
|
||||
data: ManagerStatusDataMap["SHOW_LEADERBOARD"]
|
||||
}
|
||||
|
||||
export default function Leaderboard({ data: { leaderboard } }: Props) {
|
||||
return (
|
||||
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center px-2">
|
||||
<h2 className="mb-6 text-5xl font-bold text-white drop-shadow-md">
|
||||
Leaderboard
|
||||
</h2>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{leaderboard.map(({ username, points }, key) => (
|
||||
<div
|
||||
key={key}
|
||||
className="bg-primary flex w-full justify-between rounded-md p-3 text-2xl font-bold text-white"
|
||||
>
|
||||
<span className="drop-shadow-md">{username}</span>
|
||||
<span className="drop-shadow-md">{points}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
204
packages/web/src/components/game/states/Podium.tsx
Normal file
204
packages/web/src/components/game/states/Podium.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client"
|
||||
|
||||
import { ManagerStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import useScreenSize from "@rahoot/web/hook/useScreenSize"
|
||||
import {
|
||||
SFX_PODIUM_FIRST,
|
||||
SFX_PODIUM_SECOND,
|
||||
SFX_PODIUM_THREE,
|
||||
SFX_SNEAR_ROOL,
|
||||
} from "@rahoot/web/utils/constants"
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useState } from "react"
|
||||
import ReactConfetti from "react-confetti"
|
||||
import useSound from "use-sound"
|
||||
|
||||
type Props = {
|
||||
data: ManagerStatusDataMap["FINISHED"]
|
||||
}
|
||||
|
||||
export default function Podium({ data: { subject, top } }: Props) {
|
||||
const [apparition, setApparition] = useState(0)
|
||||
|
||||
const { width, height } = useScreenSize()
|
||||
|
||||
const [sfxtThree] = useSound(SFX_PODIUM_THREE, {
|
||||
volume: 0.2,
|
||||
})
|
||||
|
||||
const [sfxSecond] = useSound(SFX_PODIUM_SECOND, {
|
||||
volume: 0.2,
|
||||
})
|
||||
|
||||
const [sfxRool, { stop: sfxRoolStop }] = useSound(SFX_SNEAR_ROOL, {
|
||||
volume: 0.2,
|
||||
})
|
||||
|
||||
const [sfxFirst] = useSound(SFX_PODIUM_FIRST, {
|
||||
volume: 0.2,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
console.log(apparition)
|
||||
|
||||
switch (apparition) {
|
||||
case 4:
|
||||
sfxRoolStop()
|
||||
sfxFirst()
|
||||
|
||||
break
|
||||
|
||||
case 3:
|
||||
sfxRool()
|
||||
|
||||
break
|
||||
|
||||
case 2:
|
||||
sfxSecond()
|
||||
|
||||
break
|
||||
|
||||
case 1:
|
||||
sfxtThree()
|
||||
|
||||
break
|
||||
}
|
||||
}, [apparition, sfxFirst, sfxSecond, sfxtThree, sfxRool, sfxRoolStop])
|
||||
|
||||
useEffect(() => {
|
||||
if (top.length < 3) {
|
||||
setApparition(4)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (apparition > 4) {
|
||||
clearInterval(interval)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setApparition((value) => value + 1)
|
||||
}, 2000)
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearInterval(interval)
|
||||
}, [apparition, top.length])
|
||||
|
||||
return (
|
||||
<>
|
||||
{apparition >= 4 && (
|
||||
<ReactConfetti
|
||||
width={width}
|
||||
height={height}
|
||||
className="h-full w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{apparition >= 3 && top.length >= 3 && (
|
||||
<div className="pointer-events-none absolute min-h-screen w-full overflow-hidden">
|
||||
<div className="spotlight"></div>
|
||||
</div>
|
||||
)}
|
||||
<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
|
||||
style={{ gridTemplateColumns: `repeat(${top.length}, 1fr)` }}
|
||||
className={`grid w-full max-w-[800px] flex-1 items-end justify-center justify-self-end overflow-x-visible overflow-y-hidden`}
|
||||
>
|
||||
{top[1] && (
|
||||
<div
|
||||
className={clsx(
|
||||
"z-20 flex h-[50%] w-full translate-y-full flex-col items-center justify-center gap-3 opacity-0 transition-all",
|
||||
{ "!translate-y-0 opacity-100": apparition >= 2 },
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={clsx(
|
||||
"overflow-visible text-center text-2xl font-bold whitespace-nowrap text-white drop-shadow-lg md:text-4xl",
|
||||
{
|
||||
"anim-balanced": apparition >= 4,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{top[1].username}
|
||||
</p>
|
||||
<div className="bg-primary flex h-full w-full flex-col items-center gap-4 rounded-t-md 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 translate-y-full flex-col items-center gap-3 opacity-0 transition-all",
|
||||
{
|
||||
"!translate-y-0 opacity-100": apparition >= 3,
|
||||
},
|
||||
{
|
||||
"md:min-w-64": top.length < 2,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={clsx(
|
||||
"overflow-visible text-center text-2xl font-bold whitespace-nowrap text-white opacity-0 drop-shadow-lg md:text-4xl",
|
||||
{ "anim-balanced opacity-100": apparition >= 4 },
|
||||
)}
|
||||
>
|
||||
{top[0].username}
|
||||
</p>
|
||||
<div className="bg-primary flex h-full w-full flex-col items-center gap-4 rounded-t-md 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 translate-y-full flex-col items-center gap-3 opacity-0 transition-all",
|
||||
{
|
||||
"!translate-y-0 opacity-100": apparition >= 1,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={clsx(
|
||||
"overflow-visible text-center text-2xl font-bold whitespace-nowrap text-white drop-shadow-lg md:text-4xl",
|
||||
{
|
||||
"anim-balanced": apparition >= 4,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{top[2].username}
|
||||
</p>
|
||||
<div className="bg-primary flex h-full w-full flex-col items-center gap-4 rounded-t-md 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
33
packages/web/src/components/game/states/Prepared.tsx
Normal file
33
packages/web/src/components/game/states/Prepared.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { CommonStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import { ANSWERS_COLORS, ANSWERS_ICONS } from "@rahoot/web/utils/constants"
|
||||
import clsx from "clsx"
|
||||
import { createElement } from "react"
|
||||
|
||||
type Props = {
|
||||
data: CommonStatusDataMap["SHOW_PREPARED"]
|
||||
}
|
||||
|
||||
export default function Prepared({
|
||||
data: { totalAnswers, questionNumber },
|
||||
}: Props) {
|
||||
return (
|
||||
<section className="anim-show relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center">
|
||||
<h2 className="anim-show mb-20 text-center text-3xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
|
||||
Question #{questionNumber}
|
||||
</h2>
|
||||
<div className="anim-quizz grid aspect-square w-60 grid-cols-2 gap-4 rounded-2xl bg-gray-700 p-5 md:w-60">
|
||||
{[...Array(totalAnswers)].map((_, key) => (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx(
|
||||
"button shadow-inset flex aspect-square h-full w-full items-center justify-center rounded-2xl",
|
||||
ANSWERS_COLORS[key],
|
||||
)}
|
||||
>
|
||||
{createElement(ANSWERS_ICONS[key], { className: "h-10 md:h-14" })}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
42
packages/web/src/components/game/states/Question.tsx
Normal file
42
packages/web/src/components/game/states/Question.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
|
||||
import { CommonStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import { SFX_SHOW_SOUND } from "@rahoot/web/utils/constants"
|
||||
import { useEffect } from "react"
|
||||
import useSound from "use-sound"
|
||||
|
||||
type Props = {
|
||||
data: CommonStatusDataMap["SHOW_QUESTION"]
|
||||
}
|
||||
|
||||
export default function Question({
|
||||
data: { question, image, cooldown },
|
||||
}: Props) {
|
||||
const [sfxShow] = useSound(SFX_SHOW_SOUND, { volume: 0.5 })
|
||||
|
||||
useEffect(() => {
|
||||
sfxShow()
|
||||
}, [sfxShow])
|
||||
|
||||
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 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>
|
||||
|
||||
{Boolean(image) && (
|
||||
<img
|
||||
alt={question}
|
||||
src={image}
|
||||
className="h-48 max-h-60 w-auto rounded-md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="bg-primary mb-20 h-4 self-start justify-self-end rounded-full"
|
||||
style={{ animation: `progressBar ${cooldown}s linear forwards` }}
|
||||
></div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
125
packages/web/src/components/game/states/Responses.tsx
Normal file
125
packages/web/src/components/game/states/Responses.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client"
|
||||
|
||||
import { ManagerStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import {
|
||||
ANSWERS_COLORS,
|
||||
ANSWERS_ICONS,
|
||||
SFX_ANSWERS_MUSIC,
|
||||
SFX_RESULTS_SOUND,
|
||||
} from "@rahoot/web/utils/constants"
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useState } from "react"
|
||||
import useSound from "use-sound"
|
||||
import AnswerButton from "../../AnswerButton"
|
||||
|
||||
const calculatePercentages = (
|
||||
objectResponses: Record<string, number>,
|
||||
): Record<string, string> => {
|
||||
const keys = Object.keys(objectResponses)
|
||||
const values = Object.values(objectResponses)
|
||||
|
||||
if (!values.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const totalSum = values.reduce(
|
||||
(accumulator, currentValue) => accumulator + currentValue,
|
||||
0,
|
||||
)
|
||||
|
||||
const result: Record<string, string> = {}
|
||||
|
||||
keys.forEach((key) => {
|
||||
result[key] = `${((objectResponses[key] / totalSum) * 100).toFixed()}%`
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type Props = {
|
||||
data: ManagerStatusDataMap["SHOW_RESPONSES"]
|
||||
}
|
||||
|
||||
export default function Responses({
|
||||
data: { question, answers, responses, correct },
|
||||
}: Props) {
|
||||
const [percentages, setPercentages] = useState<Record<string, string>>({})
|
||||
const [isMusicPlaying, setIsMusicPlaying] = useState(false)
|
||||
|
||||
const [sfxResults] = useSound(SFX_RESULTS_SOUND, {
|
||||
volume: 0.2,
|
||||
})
|
||||
|
||||
const [playMusic, { stop: stopMusic }] = useSound(SFX_ANSWERS_MUSIC, {
|
||||
volume: 0.2,
|
||||
onplay: () => {
|
||||
setIsMusicPlaying(true)
|
||||
},
|
||||
onend: () => {
|
||||
setIsMusicPlaying(false)
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
stopMusic()
|
||||
sfxResults()
|
||||
|
||||
setPercentages(calculatePercentages(responses))
|
||||
}, [responses, playMusic, stopMusic, sfxResults])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMusicPlaying) {
|
||||
playMusic()
|
||||
}
|
||||
}, [isMusicPlaying, playMusic])
|
||||
|
||||
useEffect(() => {
|
||||
stopMusic()
|
||||
}, [playMusic, stopMusic])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-1 flex-col justify-between">
|
||||
<div className="mx-auto inline-flex h-full w-full max-w-7xl flex-1 flex-col items-center justify-center gap-5">
|
||||
<h2 className="text-center text-2xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
|
||||
{question}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className={`mt-8 grid h-40 w-full max-w-3xl gap-4 px-2`}
|
||||
style={{ gridTemplateColumns: `repeat(${answers.length}, 1fr)` }}
|
||||
>
|
||||
{answers.map((_, key) => (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx(
|
||||
"flex flex-col justify-end self-end overflow-hidden rounded-md",
|
||||
ANSWERS_COLORS[key],
|
||||
)}
|
||||
style={{ height: percentages[key] }}
|
||||
>
|
||||
<span className="w-full bg-black/10 text-center text-lg font-bold text-white drop-shadow-md">
|
||||
{responses[key] || 0}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mx-auto mb-4 grid w-full max-w-7xl grid-cols-2 gap-1 rounded-full px-2 text-lg font-bold text-white md:text-xl">
|
||||
{answers.map((answer, key) => (
|
||||
<AnswerButton
|
||||
key={key}
|
||||
className={clsx(ANSWERS_COLORS[key], {
|
||||
"opacity-65": responses && correct !== key,
|
||||
})}
|
||||
icon={ANSWERS_ICONS[key]}
|
||||
>
|
||||
{answer}
|
||||
</AnswerButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
packages/web/src/components/game/states/Result.tsx
Normal file
50
packages/web/src/components/game/states/Result.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { CommonStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import CricleCheck from "@rahoot/web/components/icons/CricleCheck"
|
||||
import CricleXmark from "@rahoot/web/components/icons/CricleXmark"
|
||||
import { usePlayerStore } from "@rahoot/web/stores/player"
|
||||
import { SFX_RESULTS_SOUND } from "@rahoot/web/utils/constants"
|
||||
import { useEffect } from "react"
|
||||
import useSound from "use-sound"
|
||||
|
||||
type Props = {
|
||||
data: CommonStatusDataMap["SHOW_RESULT"]
|
||||
}
|
||||
|
||||
export default function Result({
|
||||
data: { correct, message, points, myPoints, rank, aheadOfMe },
|
||||
}: Props) {
|
||||
const player = usePlayerStore()
|
||||
|
||||
const [sfxResults] = useSound(SFX_RESULTS_SOUND, {
|
||||
volume: 0.2,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
player.updatePoints(myPoints)
|
||||
|
||||
sfxResults()
|
||||
}, [sfxResults])
|
||||
|
||||
return (
|
||||
<section className="anim-show relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center">
|
||||
{correct ? (
|
||||
<CricleCheck className="aspect-square max-h-60 w-full" />
|
||||
) : (
|
||||
<CricleXmark className="aspect-square max-h-60 w-full" />
|
||||
)}
|
||||
<h2 className="mt-1 text-4xl font-bold text-white drop-shadow-lg">
|
||||
{message}
|
||||
</h2>
|
||||
<p className="mt-1 text-xl font-bold text-white drop-shadow-lg">
|
||||
{`You are top ${rank}${aheadOfMe ? `, behind ${aheadOfMe}` : ""}`}
|
||||
</p>
|
||||
{correct && (
|
||||
<span className="mt-2 rounded bg-black/40 px-4 py-2 text-2xl font-bold text-white drop-shadow-lg">
|
||||
+{points}
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
70
packages/web/src/components/game/states/Room.tsx
Normal file
70
packages/web/src/components/game/states/Room.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import { Player } from "@rahoot/common/types/game"
|
||||
import { ManagerStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import { useEvent, useSocket } from "@rahoot/web/contexts/socketProvider"
|
||||
import { useState } from "react"
|
||||
|
||||
type Props = {
|
||||
data: ManagerStatusDataMap["SHOW_ROOM"]
|
||||
}
|
||||
|
||||
export default function Room({ data: { text, inviteCode } }: Props) {
|
||||
const { socket } = useSocket()
|
||||
const [playerList, setPlayerList] = useState<Player[]>([])
|
||||
const [totalPlayers, setTotalPlayers] = useState(0)
|
||||
|
||||
useEvent("manager:newPlayer", (player) => {
|
||||
setPlayerList([...playerList, player])
|
||||
})
|
||||
|
||||
useEvent("manager:removePlayer", (playerId) => {
|
||||
setPlayerList(playerList.filter((p) => p.id !== playerId))
|
||||
})
|
||||
|
||||
useEvent("manager:playerKicked", (playerId) => {
|
||||
setPlayerList(playerList.filter((p) => p.id !== playerId))
|
||||
})
|
||||
|
||||
useEvent("game:totalPlayers", (total) => {
|
||||
setTotalPlayers(total)
|
||||
})
|
||||
|
||||
const handleKick = (playerId: string) => () => {
|
||||
socket?.emit("manager:kickPlayer", {
|
||||
data: { playerId },
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center px-2">
|
||||
<div className="mb-10 rotate-3 rounded-md bg-white px-6 py-4 text-6xl font-extrabold">
|
||||
{inviteCode}
|
||||
</div>
|
||||
|
||||
<h2 className="mb-4 text-4xl font-bold text-white drop-shadow-lg">
|
||||
{text}
|
||||
</h2>
|
||||
|
||||
<div className="mb-6 flex items-center justify-center rounded-full bg-black/40 px-6 py-3">
|
||||
<span className="text-2xl font-bold text-white drop-shadow-md">
|
||||
Players Joined: {totalPlayers}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{playerList.map((player) => (
|
||||
<div
|
||||
key={player.id}
|
||||
className="shadow-inset bg-primary rounded-md px-4 py-3 font-bold text-white"
|
||||
onClick={handleKick(player.id)}
|
||||
>
|
||||
<span className="cursor-pointer text-xl drop-shadow-md hover:line-through">
|
||||
{player.username}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
55
packages/web/src/components/game/states/Start.tsx
Normal file
55
packages/web/src/components/game/states/Start.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import { CommonStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import { useEvent } from "@rahoot/web/contexts/socketProvider"
|
||||
import { SFX_BOUMP_SOUND } from "@rahoot/web/utils/constants"
|
||||
import clsx from "clsx"
|
||||
import { useState } from "react"
|
||||
import useSound from "use-sound"
|
||||
|
||||
type Props = {
|
||||
data: CommonStatusDataMap["SHOW_START"]
|
||||
}
|
||||
|
||||
export default function Start({ data: { time, subject } }: Props) {
|
||||
const [showTitle, setShowTitle] = useState(true)
|
||||
const [cooldown, setCooldown] = useState(time)
|
||||
|
||||
const [sfxBoump] = useSound(SFX_BOUMP_SOUND, {
|
||||
volume: 0.2,
|
||||
})
|
||||
|
||||
useEvent("game:startCooldown", () => {
|
||||
sfxBoump()
|
||||
setShowTitle(false)
|
||||
})
|
||||
|
||||
useEvent("game:cooldown", (sec) => {
|
||||
sfxBoump()
|
||||
setCooldown(sec)
|
||||
})
|
||||
|
||||
return (
|
||||
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center">
|
||||
{showTitle ? (
|
||||
<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={clsx(
|
||||
`anim-show bg-primary aspect-square h-32 transition-all md:h-60`,
|
||||
)}
|
||||
style={{
|
||||
transform: `rotate(${45 * (time - cooldown)}deg)`,
|
||||
}}
|
||||
></div>
|
||||
<span className="absolute text-6xl font-bold text-white drop-shadow-md md:text-8xl">
|
||||
{cooldown}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
17
packages/web/src/components/game/states/Wait.tsx
Normal file
17
packages/web/src/components/game/states/Wait.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PlayerStatusDataMap } from "@rahoot/common/types/game/status"
|
||||
import Loader from "@rahoot/web/components/Loader"
|
||||
|
||||
type Props = {
|
||||
data: PlayerStatusDataMap["WAIT"]
|
||||
}
|
||||
|
||||
export default function Wait({ data: { text } }: Props) {
|
||||
return (
|
||||
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center">
|
||||
<Loader />
|
||||
<h2 className="mt-5 text-center text-3xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
|
||||
{text}
|
||||
</h2>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
30
packages/web/src/components/icons/Circle.tsx
Normal file
30
packages/web/src/components/icons/Circle.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
type Props = {
|
||||
className?: string
|
||||
fill?: string
|
||||
}
|
||||
|
||||
export default function Circle({ className, fill = "#FFF" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
id="Page-1"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="icon" fill={fill} transform="translate(42.666667, 42.666667)">
|
||||
<path
|
||||
d="M213.333333,3.55271368e-14 C331.15408,3.55271368e-14 426.666667,95.5125867 426.666667,213.333333 C426.666667,331.15408 331.15408,426.666667 213.333333,426.666667 C95.5125867,426.666667 3.55271368e-14,331.15408 3.55271368e-14,213.333333 C3.55271368e-14,95.5125867 95.5125867,3.55271368e-14 213.333333,3.55271368e-14 Z"
|
||||
id="Combined-Shape"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
41
packages/web/src/components/icons/CricleCheck.tsx
Normal file
41
packages/web/src/components/icons/CricleCheck.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function CricleCheck({ className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill="#22c55e"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 56.00 56.00"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#22c55e"
|
||||
strokeWidth="0.00056"
|
||||
className={className}
|
||||
>
|
||||
<g strokeWidth="0" transform="translate(11.2,11.2), scale(0.6)">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="56.00"
|
||||
height="56.00"
|
||||
rx="28"
|
||||
fill="#ffffff"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<g
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="#CCCCCC"
|
||||
strokeWidth="0.6719999999999999"
|
||||
/>
|
||||
|
||||
<g>
|
||||
<path d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0312 4.0937 27.9765 4.0937 C 14.8983 4.0937 4.0937 14.9453 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 24.7655 40.0234 C 23.9687 40.0234 23.3593 39.6719 22.6796 38.8750 L 15.9296 30.5312 C 15.5780 30.0859 15.3671 29.5234 15.3671 29.0078 C 15.3671 27.9063 16.2343 27.0625 17.2655 27.0625 C 17.9452 27.0625 18.5077 27.3203 19.0702 28.0469 L 24.6718 35.2890 L 35.5702 17.8281 C 36.0155 17.1016 36.6249 16.75 37.2343 16.75 C 38.2655 16.75 39.2733 17.4297 39.2733 18.5547 C 39.2733 19.0703 38.9687 19.6328 38.6640 20.1016 L 26.7577 38.8750 C 26.2421 39.6484 25.5858 40.0234 24.7655 40.0234 Z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
35
packages/web/src/components/icons/CricleXmark.tsx
Normal file
35
packages/web/src/components/icons/CricleXmark.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function CricleXmark({ className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill="#ef4444"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 56.00 56.00"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#ef4444"
|
||||
className={className}
|
||||
>
|
||||
<g strokeWidth="0" transform="translate(12.4,12.4), scale(0.6)">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="56.00"
|
||||
height="56.00"
|
||||
rx="28"
|
||||
fill="#ffffff"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<g strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
<g>
|
||||
<path d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0312 4.0937 27.9765 4.0937 C 14.8983 4.0937 4.0937 14.9453 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 19.5858 38.4063 C 18.4843 38.4063 17.5936 37.5156 17.5936 36.4141 C 17.5936 35.8750 17.8280 35.4063 18.2030 35.0547 L 25.1874 28.0234 L 18.2030 20.9922 C 17.8280 20.6641 17.5936 20.1719 17.5936 19.6328 C 17.5936 18.5547 18.4843 17.6875 19.5858 17.6875 C 20.1249 17.6875 20.5936 17.8984 20.9452 18.2734 L 27.9765 25.2812 L 35.0546 18.25 C 35.4530 17.8281 35.8749 17.6406 36.3905 17.6406 C 37.4921 17.6406 38.3827 18.5312 38.3827 19.6094 C 38.3827 20.1484 38.1952 20.5937 37.7968 20.9688 L 30.7655 28.0234 L 37.7733 35.0078 C 38.1249 35.3828 38.3593 35.8516 38.3593 36.4141 C 38.3593 37.5156 37.4687 38.4063 36.3671 38.4063 C 35.8046 38.4063 35.3358 38.1719 34.9843 37.8203 L 27.9765 30.7890 L 20.9921 37.8203 C 20.6405 38.1953 20.1249 38.4063 19.5858 38.4063 Z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
46
packages/web/src/components/icons/Pentagon.tsx
Normal file
46
packages/web/src/components/icons/Pentagon.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
type Props = {
|
||||
className?: string
|
||||
fill?: string
|
||||
stroke?: string
|
||||
}
|
||||
|
||||
export default function Pentagon({ className, fill, stroke }: Props) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
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>
|
||||
)
|
||||
}
|
||||
24
packages/web/src/components/icons/Rhombus.tsx
Normal file
24
packages/web/src/components/icons/Rhombus.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
type Props = {
|
||||
className?: string
|
||||
fill?: string
|
||||
}
|
||||
|
||||
export default function Rhombus({ className, fill = "#FFF" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="-56.32 -56.32 624.64 624.64"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
transform="rotate(45)"
|
||||
>
|
||||
<g strokeWidth="0" />
|
||||
|
||||
<g strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
<g>
|
||||
<rect x="48" y="48" width="416" height="416" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
17
packages/web/src/components/icons/Square.tsx
Normal file
17
packages/web/src/components/icons/Square.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
type Props = {
|
||||
className?: string
|
||||
fill?: string
|
||||
}
|
||||
|
||||
export default function Square({ className, fill = "#FFF" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="48" y="48" width="416" height="416" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
17
packages/web/src/components/icons/Triangle.tsx
Normal file
17
packages/web/src/components/icons/Triangle.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
type Props = {
|
||||
className?: string
|
||||
fill?: string
|
||||
}
|
||||
|
||||
export default function Triangle({ className, fill = "#FFF" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={fill}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polygon points="256 32 20 464 492 464 256 32" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user