Add Start cooldown & Patch socket event

This commit is contained in:
Ralex91
2024-02-02 13:55:55 +01:00
parent 6c7f31a6d4
commit cd723f7efd
19 changed files with 298 additions and 151 deletions

View File

@@ -17,6 +17,10 @@ io.listen(5157)
io.on("connection", (socket) => { io.on("connection", (socket) => {
console.log(`A user connected ${socket.id}`) console.log(`A user connected ${socket.id}`)
socket.on("player:checkRoom", (roomId) =>
Player.checkRoom(gameState, io, socket, roomId),
)
socket.on("player:join", (player) => socket.on("player:join", (player) =>
Player.join(gameState, io, socket, player), Player.join(gameState, io, socket, player),
) )

View File

@@ -1,6 +1,7 @@
import { GAME_STATE_INIT } from "../quizz.config.js" import { GAME_STATE_INIT } from "../quizz.config.js"
import { startRound } from "../utils/round.js" import { startRound } from "../utils/round.js"
import generateRoomId from "../utils/generateRoomId.js" import generateRoomId from "../utils/generateRoomId.js"
import { cooldown, sleep } from "../utils/cooldown.js"
const Manager = { const Manager = {
createRoom: (game, io, socket) => { createRoom: (game, io, socket) => {
@@ -31,13 +32,24 @@ const Manager = {
io.to(game.manager).emit("manager:playerKicked", player.id) io.to(game.manager).emit("manager:playerKicked", player.id)
}, },
startGame: (game, io, socket) => { startGame: async (game, io, socket) => {
if (game.started || !game.room) { if (game.started || !game.room) {
return return
} }
game.started = true game.started = true
io.to(game.room).emit("startGame", game.room) io.to(game.room).emit("game:status", {
name: "SHOW_START",
data: {
time: 3,
subject: "Adobe",
},
})
await sleep(3)
io.to(game.room).emit("game:startCooldown")
await cooldown(3, io, game.room)
startRound(game, io, socket) startRound(game, io, socket)
}, },

View File

@@ -1,7 +1,17 @@
import convertTimeToPoint from "../utils/convertTimeToPoint.js" import convertTimeToPoint from "../utils/convertTimeToPoint.js"
import { abortCooldown } from "../utils/round.js" import { abortCooldown } from "../utils/cooldown.js"
const Player = { const Player = {
checkRoom: (game, io, socket, roomId) => {
if (!game.room || roomId !== game.room) {
io.to(socket.id).emit("game:errorMessage", "Room not found")
console.log("zaza")
return
}
io.to(socket.id).emit("game:successRoom", roomId)
},
join: (game, io, socket, player) => { join: (game, io, socket, player) => {
if (!player.room || !player.room || game.started) { if (!player.room || !player.room || game.started) {
return return

View File

@@ -0,0 +1,26 @@
let cooldownTimeout
let cooldownResolve
export const abortCooldown = () => {
clearInterval(cooldownTimeout)
cooldownResolve()
}
export const cooldown = (ms, io, room) => {
let count = ms - 1
return new Promise((resolve) => {
cooldownResolve = resolve
cooldownTimeout = setInterval(() => {
if (!count) {
clearInterval(cooldownTimeout)
resolve()
}
io.to(room).emit("game:cooldown", count)
count -= 1
}, 1000)
})
}
export const sleep = (sec) => new Promise((r) => setTimeout(r, sec * 1000))

View File

@@ -1,29 +1,4 @@
let cooldownTimeout import { cooldown, sleep } from "./cooldown.js"
let cooldownResolve
export const abortCooldown = () => {
clearInterval(cooldownTimeout)
cooldownResolve()
}
function cooldown(ms, io, room) {
let count = ms - 1
return new Promise((resolve) => {
cooldownResolve = resolve
cooldownTimeout = setInterval(() => {
if (!count) {
clearInterval(cooldownTimeout)
resolve()
}
io.to(room).emit("game:cooldown", count)
count -= 1
}, 1000)
})
}
const sleep = (sec) => new Promise((r) => setTimeout(r, sec * 1000))
export const startRound = async (game, io, socket) => { export const startRound = async (game, io, socket) => {
const question = game.questions[game.currentQuestion] const question = game.questions[game.currentQuestion]

View File

@@ -3,7 +3,7 @@ import Button from "@/components/Button"
import background from "@/assets/background.webp" import background from "@/assets/background.webp"
import { usePlayerContext } from "@/context/player" import { usePlayerContext } from "@/context/player"
import { useSocketContext } from "@/context/socket" import { useSocketContext } from "@/context/socket"
import { useState } from "react" import { useEffect, useState } from "react"
export default function GameWrapper({ children, textNext, onNext, manager }) { export default function GameWrapper({ children, textNext, onNext, manager }) {
const { socket } = useSocketContext() const { socket } = useSocketContext()
@@ -11,6 +11,7 @@ export default function GameWrapper({ children, textNext, onNext, manager }) {
const [questionState, setQuestionState] = useState() const [questionState, setQuestionState] = useState()
useEffect(() => {
socket.on("game:updateQuestion", ({ current, total }) => { socket.on("game:updateQuestion", ({ current, total }) => {
setQuestionState({ setQuestionState({
current, current,
@@ -18,12 +19,18 @@ export default function GameWrapper({ children, textNext, onNext, manager }) {
}) })
}) })
return () => {
socket.off("game:updateQuestion")
}
}, [])
return ( return (
<section className="relative flex min-h-screen w-full flex-col justify-between"> <section className="relative flex min-h-screen w-full flex-col justify-between">
<div className="fixed left-0 top-0 -z-10 h-full w-full bg-orange-600 opacity-70"> <div className="fixed left-0 top-0 -z-10 h-full w-full bg-orange-600 opacity-70">
<Image <Image
className="pointer-events-none h-full w-full object-cover opacity-60" className="pointer-events-none h-full w-full object-cover opacity-60"
src={background} src={background}
alt="background"
/> />
</div> </div>
@@ -47,7 +54,7 @@ export default function GameWrapper({ children, textNext, onNext, manager }) {
{children} {children}
{!manager && ( {!manager && (
<div className="flex items-center justify-between bg-white px-4 py-2 text-lg font-bold text-white"> <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 && player.username}</p> <p className="text-gray-800">{!!player && player.username}</p>
<div className="rounded-sm bg-gray-800 px-3 py-1 text-lg"> <div className="rounded-sm bg-gray-800 px-3 py-1 text-lg">
{!!player && player.points} {!!player && player.points}

View File

@@ -2,17 +2,28 @@ import { usePlayerContext } from "@/context/player"
import Form from "@/components/Form" import Form from "@/components/Form"
import Button from "@/components/Button" import Button from "@/components/Button"
import Input from "@/components/Input" import Input from "@/components/Input"
import { useState } from "react" import { useEffect, useMemo, useState } from "react"
import { socket } from "@/context/socket"
export default function Room() { export default function Room() {
const { player, dispatch } = usePlayerContext() const { player, dispatch } = usePlayerContext()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [roomId, setRoomId] = useState("207223") const [roomId, setRoomId] = useState("")
const handleLogin = () => { const handleLogin = () => {
dispatch({ type: "JOIN", payload: roomId }) socket.emit("player:checkRoom", roomId)
} }
useEffect(() => {
socket.on("game:successRoom", (roomId) => {
dispatch({ type: "JOIN", payload: roomId })
})
return () => {
socket.off("game:successRoom")
}
}, [])
return ( return (
<> <>
{loading && ( {loading && (

View File

@@ -1,21 +1,9 @@
import Circle from "@/components/icons/Circle"
import Triangle from "@/components/icons/Triangle"
import Square from "@/components/icons/Square"
import Rhombus from "@/components/icons/Rhombus"
import AnswerButton from "../../AnswerButton" import AnswerButton from "../../AnswerButton"
import { useSocketContext } from "@/context/socket" import { useSocketContext } from "@/context/socket"
import Image from "next/image" import Image from "next/image"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import clsx from "clsx" import clsx from "clsx"
import { ANSWERS_COLORS, ANSWERS_ICONS } from "@/constants"
const answersColors = [
"bg-red-500",
"bg-blue-500",
"bg-yellow-500",
"bg-green-500",
]
const answersIcons = [Triangle, Rhombus, Circle, Square]
const calculatePercentages = (objectResponses) => { const calculatePercentages = (objectResponses) => {
const keys = Object.keys(objectResponses) const keys = Object.keys(objectResponses)
@@ -36,8 +24,6 @@ const calculatePercentages = (objectResponses) => {
result[key] = ((objectResponses[key] / totalSum) * 100).toFixed() + "%" result[key] = ((objectResponses[key] / totalSum) * 100).toFixed() + "%"
}) })
console.log(result)
return result return result
} }
@@ -58,6 +44,7 @@ export default function Answers({
setPercentages(calculatePercentages(responses)) setPercentages(calculatePercentages(responses))
}, [responses]) }, [responses])
useEffect(() => {
socket.on("game:cooldown", (sec) => { socket.on("game:cooldown", (sec) => {
setCooldown(sec) setCooldown(sec)
}) })
@@ -66,6 +53,12 @@ export default function Answers({
setTotalAnswer(count) setTotalAnswer(count)
}) })
return () => {
socket.off("game:cooldown")
socket.off("game:playerAnswer")
}
}, [])
return ( return (
<div className="flex h-full flex-1 flex-col justify-between"> <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"> <div className="mx-auto inline-flex h-full w-full max-w-7xl flex-1 flex-col items-center justify-center gap-5">
@@ -84,7 +77,7 @@ export default function Answers({
key={key} key={key}
className={clsx( className={clsx(
"flex flex-col justify-end self-end overflow-hidden rounded-md", "flex flex-col justify-end self-end overflow-hidden rounded-md",
answersColors[key], ANSWERS_COLORS[key],
)} )}
style={{ height: percentages[key] }} style={{ height: percentages[key] }}
> >
@@ -115,10 +108,10 @@ export default function Answers({
{answers.map((answer, key) => ( {answers.map((answer, key) => (
<AnswerButton <AnswerButton
key={key} key={key}
className={clsx(answersColors[key], { className={clsx(ANSWERS_COLORS[key], {
"opacity-65": responses && correct !== key, "opacity-65": responses && correct !== key,
})} })}
icon={answersIcons[key]} icon={ANSWERS_ICONS[key]}
onClick={() => socket.emit("player:selectedAnswer", key)} onClick={() => socket.emit("player:selectedAnswer", key)}
> >
{answer} {answer}

View File

@@ -1,27 +1,25 @@
import Circle from "@/components/icons/Circle" import { ANSWERS_COLORS, ANSWERS_ICONS } from "@/constants"
import Rhombus from "@/components/icons/Rhombus" import clsx from "clsx"
import Square from "@/components/icons/Square" import { createElement } from "react"
import Triangle from "@/components/icons/Triangle"
export default function Prepared({ data: { totalAnswers, questionNumber } }) { export default function Prepared({ data: { totalAnswers, questionNumber } }) {
return ( return (
<section className="anim-show relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center"> <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-10 text-center text-2xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl"> <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} Question #{questionNumber}
</h2> </h2>
<div className="anim-quizz grid h-[300px] w-60 grid-cols-2 gap-4 rounded-2xl bg-gray-700 p-5 md:h-[400px] md:w-80 "> <div className="anim-quizz grid aspect-square w-60 grid-cols-2 gap-4 rounded-2xl bg-gray-700 p-5 md:w-60">
<div className="button shadow-inset flex aspect-square h-full w-full items-center justify-center rounded-2xl bg-red-500"> {[...Array(totalAnswers)].map((_, key) => (
<Triangle className="h-10 md:h-14" /> <div
</div> key={key}
<div className="button shadow-inset flex aspect-square h-full w-full items-center justify-center rounded-2xl bg-blue-500"> className={clsx(
<Rhombus className="h-10 md:h-14" /> "button shadow-inset flex aspect-square h-full w-full items-center justify-center rounded-2xl",
</div> ANSWERS_COLORS[key],
<div className="button shadow-inset flex aspect-square h-full w-full items-center justify-center rounded-2xl bg-yellow-500"> )}
<Square className="h-10 md:h-14" /> >
</div> {createElement(ANSWERS_ICONS[key], { className: "h-10 md:h-14" })}
<div className="button shadow-inset flex aspect-square h-full w-full items-center justify-center rounded-2xl bg-green-500">
<Circle className="h-10 md:h-14" />
</div> </div>
))}
</div> </div>
</section> </section>
) )

View File

@@ -6,7 +6,7 @@ export default function Question({ data: { question } }) {
return ( return (
<section className="relative mx-auto flex h-full w-full max-w-7xl flex-1 flex-col items-center px-4"> <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 items-center">
<h2 className="anim-show text-center text-2xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl"> <h2 className="anim-show text-center text-3xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
{question} {question}
</h2> </h2>
</div> </div>

View File

@@ -0,0 +1,43 @@
import { useSocketContext } from "@/context/socket"
import { useEffect, useState } from "react"
export default function Room({ data: { text, inviteCode } }) {
const { socket } = useSocketContext()
const [playerList, setPlayerList] = useState([])
useEffect(() => {
socket.on("manager:newPlayer", (player) => {
setPlayerList([...playerList, player])
})
return () => {
socket.off("manager:newPlayer")
}
}, [playerList])
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="flex flex-wrap gap-3">
{playerList.map((player) => (
<div
key={player.id}
className="shadow-inset rounded-md bg-primary px-4 py-3 font-bold text-white"
onClick={() => socket.emit("manager:kickPlayer", player.id)}
>
<span className="cursor-pointer text-xl drop-shadow-md hover:line-through">
{player.username}
</span>
</div>
))}
</div>
</section>
)
}

View File

@@ -1,37 +1,48 @@
import { useSocketContext } from "@/context/socket" import { useSocketContext } from "@/context/socket"
import { useState } from "react" import clsx from "clsx"
import { useEffect, useState } from "react"
export default function Start({ data: { text, inviteCode } }) { export default function Start({ data: { time, subject } }) {
const { socket } = useSocketContext() const { socket } = useSocketContext()
const [playerList, setPlayerList] = useState([]) const [showTitle, setShowTitle] = useState(true)
const [cooldown, setCooldown] = useState(time)
socket.on("manager:newPlayer", (player) => { useEffect(() => {
setPlayerList([...playerList, player]) socket.on("game:startCooldown", () => {
setShowTitle(false)
}) })
socket.on("game:cooldown", (sec) => {
setCooldown(sec)
})
return () => {
socket.off("game:startCooldown")
socket.off("game:cooldown")
}
}, [])
return ( return (
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center px-2"> <section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center">
<div className="mb-10 rotate-3 rounded-md bg-white px-6 py-4 text-6xl font-extrabold"> {showTitle ? (
{inviteCode} <h2 className="anim-show text-center text-3xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
</div> {subject}
<h2 className="mb-4 text-4xl font-bold text-white drop-shadow-lg">
{text}
</h2> </h2>
) : (
<div className="flex flex-wrap gap-3"> <>
{playerList.map((player) => (
<div <div
key={player.id} className={clsx(
className="shadow-inset rounded-md bg-primary px-4 py-3 font-bold text-white" `anim-show aspect-square h-32 bg-primary transition-all md:h-60`,
onClick={() => socket.emit("manager:kickPlayer", player.id)} )}
> style={{
<span className="cursor-pointer text-xl drop-shadow-md hover:line-through"> transform: `rotate(${45 * (time - cooldown)}deg)`,
{player.username} }}
></div>
<span className="absolute text-6xl font-bold text-white drop-shadow-md md:text-8xl">
{cooldown}
</span> </span>
</div> </>
))} )}
</div>
</section> </section>
) )
} }

View File

@@ -4,7 +4,7 @@ export default function Wait({ data: { text } }) {
return ( return (
<section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center"> <section className="relative mx-auto flex w-full max-w-7xl flex-1 flex-col items-center justify-center">
<Loader /> <Loader />
<h2 className="mt-5 text-4xl font-bold text-white drop-shadow-lg"> <h2 className="mt-5 text-center text-3xl font-bold text-white drop-shadow-lg md:text-4xl lg:text-5xl">
{text} {text}
</h2> </h2>
</section> </section>

View File

@@ -1 +1,15 @@
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" ///"https://rahoot.ralex.app"
export const ANSWERS_COLORS = [
"bg-red-500",
"bg-blue-500",
"bg-yellow-500",
"bg-green-500",
]
export const ANSWERS_ICONS = [Triangle, Rhombus, Circle, Square]

View File

@@ -5,15 +5,17 @@ import Prepared from "@/components/game/states/Prepared"
import Question from "@/components/game/states/Question" import Question from "@/components/game/states/Question"
import Result from "@/components/game/states/Result" import Result from "@/components/game/states/Result"
import Wait from "@/components/game/states/Wait" import Wait from "@/components/game/states/Wait"
import Start from "@/components/game/states/Start"
import { usePlayerContext } from "@/context/player" import { usePlayerContext } from "@/context/player"
import { useSocketContext } from "@/context/socket" import { useSocketContext } from "@/context/socket"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { createElement, useState } from "react" import { createElement, useMemo, useState } from "react"
const gameStateComponent = { const gameStateComponent = {
SELECT_ANSWER: Answers, SELECT_ANSWER: Answers,
SHOW_QUESTION: Question, SHOW_QUESTION: Question,
WAIT: Wait, WAIT: Wait,
SHOW_START: Start,
SHOW_RESULT: Result, SHOW_RESULT: Result,
SHOW_PREPARED: Prepared, SHOW_PREPARED: Prepared,
} }
@@ -40,6 +42,7 @@ export default function Game() {
}, },
}) })
useMemo(() => {
socket.on("game:status", (status) => { socket.on("game:status", (status) => {
setState({ setState({
...state, ...state,
@@ -51,6 +54,11 @@ export default function Game() {
}) })
}) })
return () => {
socket.off("game:status")
}
}, [state])
return ( return (
<GameWrapper> <GameWrapper>
{gameStateComponent[state.status.name] && {gameStateComponent[state.status.name] &&

View File

@@ -4,16 +4,28 @@ import Form from "@/components/Form"
import Button from "@/components/Button" import Button from "@/components/Button"
import Input from "@/components/Input" import Input from "@/components/Input"
import logo from "@/assets/logo.svg" import logo from "@/assets/logo.svg"
import { useState } from "react" import { useEffect, useMemo, useState } from "react"
import Loader from "@/components/Loader" import Loader from "@/components/Loader"
import { usePlayerContext } from "@/context/player" import { usePlayerContext } from "@/context/player"
import Room from "@/components/game/join/Room" import Room from "@/components/game/join/Room"
import Username from "@/components/game/join/Username" import Username from "@/components/game/join/Username"
import { useSocketContext } from "@/context/socket"
export default function Home() { export default function Home() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { player } = usePlayerContext() const { player, dispatch } = usePlayerContext()
const { socket } = useSocketContext()
useMemo(() => {
socket.on("game:errorMessage", (message) => {
console.log(message)
})
return () => {
socket.off("game:errorMessage")
}
}, [])
return ( return (
<section className="relative flex min-h-screen flex-col items-center justify-center"> <section className="relative flex min-h-screen flex-col items-center justify-center">
@@ -28,7 +40,7 @@ export default function Home() {
</div> </div>
)} )}
<Image src={logo} className="mb-6 h-32" /> <Image src={logo} className="mb-6 h-32" alt="logo" />
{!player ? <Room /> : <Username />} {!player ? <Room /> : <Username />}
</section> </section>

View File

@@ -4,14 +4,16 @@ import Answers from "@/components/game/states/Answers"
import Leaderboard from "@/components/game/states/Leaderboard" import Leaderboard from "@/components/game/states/Leaderboard"
import Prepared from "@/components/game/states/Prepared" import Prepared from "@/components/game/states/Prepared"
import Question from "@/components/game/states/Question" import Question from "@/components/game/states/Question"
import Room from "@/components/game/states/Room"
import Start from "@/components/game/states/Start" import Start from "@/components/game/states/Start"
import Wait from "@/components/game/states/Wait" import Wait from "@/components/game/states/Wait"
import { usePlayerContext } from "@/context/player" import { usePlayerContext } from "@/context/player"
import { useSocketContext } from "@/context/socket" import { useSocketContext } from "@/context/socket"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { createElement, useState } from "react" import { createElement, useEffect, useState } from "react"
const gameStateComponent = { const gameStateComponent = {
SHOW_ROOM: Room,
SHOW_START: Start, SHOW_START: Start,
SELECT_ANSWER: Answers, SELECT_ANSWER: Answers,
SHOW_QUESTION: Question, SHOW_QUESTION: Question,
@@ -23,13 +25,12 @@ const gameStateComponent = {
export default function Manager() { export default function Manager() {
const { socket } = useSocketContext() const { socket } = useSocketContext()
const { player } = usePlayerContext()
const [nextText, setNextText] = useState("Start") const [nextText, setNextText] = useState("Start")
const [state, setState] = useState({ const [state, setState] = useState({
created: false, created: false,
status: { status: {
name: "SHOW_START", name: "SHOW_ROOM",
data: { text: "Waiting for the players" }, data: { text: "Waiting for the players" },
}, },
question: { question: {
@@ -38,6 +39,7 @@ export default function Manager() {
}, },
}) })
useEffect(() => {
socket.on("game:status", (status) => { socket.on("game:status", (status) => {
setState({ setState({
...state, ...state,
@@ -63,6 +65,12 @@ export default function Manager() {
}) })
}) })
return () => {
socket.off("game:status")
socket.off("manager:inviteCode")
}
}, [state])
const handleCreate = () => { const handleCreate = () => {
socket.emit("manager:createRoom") socket.emit("manager:createRoom")
} }
@@ -71,7 +79,7 @@ export default function Manager() {
setNextText("Skip") setNextText("Skip")
switch (state.status.name) { switch (state.status.name) {
case "SHOW_START": case "SHOW_ROOM":
socket.emit("manager:startGame") socket.emit("manager:startGame")
break break

View File

@@ -48,6 +48,7 @@
} }
.anim-quizz .button { .anim-quizz .button {
box-shadow: rgba(0, 0, 0, 0.25) -4px -4px inset;
animation: quizzButton 0.8s ease-out; animation: quizzButton 0.8s ease-out;
} }
@@ -94,7 +95,14 @@
0% { 0% {
transform: scale(0) perspective(1200px) rotateY(-60deg) rotateX(60deg) transform: scale(0) perspective(1200px) rotateY(-60deg) rotateX(60deg)
translateZ(100px); translateZ(100px);
box-shadow: 40px 10px 0 rgba(20, 24, 29, 1); }
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% { 100% {
transform: scale(1) perspective(1200px) rotateY(-15deg) rotateX(15deg) transform: scale(1) perspective(1200px) rotateY(-15deg) rotateX(15deg)
@@ -106,6 +114,12 @@
0% { 0% {
transform: scale(0); transform: scale(0);
} }
60% {
transform: scale(1);
}
80% {
transform: scale(0.8);
}
100% { 100% {
transform: scale(1); transform: scale(1);
} }

View File

@@ -13,6 +13,7 @@ module.exports = {
}, },
}, },
}, },
safelist: ["grid-cols-4", "grid-cols-3", "grid-cols-2"], safelist: ["grid-cols-4", "grid-cols-3", "grid-cols-2", {
pattern: /bg-(red|blue|yellow|green)/}],
plugins: [], plugins: [],
} }