First commit

This commit is contained in:
Lucien Astié 2024-08-08 22:21:01 +02:00
commit 48866cc6ab
20 changed files with 6301 additions and 0 deletions

3
.eslintrc.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

0
Dockerfile Normal file
View file

36
README.md Normal file
View file

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

7
app/[id]/forceUpdate.tsx Normal file
View file

@ -0,0 +1,7 @@
'use client'
import { useState } from 'react'
export default function UseForceUpdate() {
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
}

274
app/[id]/page.tsx Normal file
View file

@ -0,0 +1,274 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { io } from "socket.io-client"
import { getRandomQuestion } from "./questions"
import { useForceUpdate } from "./forceUpdate"
import { IconCrown } from "@tabler/icons-react"
interface roomProps {
params: {
id: string;
};
}
export default function Home({ params }: roomProps) {
const [isConnected, setIsConnected] = useState(false)
const [forceUpdate, setForceUpdate] = useState(0)
const [role, setRole] = useState("")
const [name, setName] = useState("")
const [gameStarted, setGameStarted] = useState(false)
const [gameEnded, setGameEnded] = useState(false)
const [questionDisplayed, setQuestionDisplayed] = useState("")
const [possibleChoice, setPossibleChoice] = useState([])
const [totalVotes, setTotalVotes] = useState(0)
const [choice, setChoice] = useState("")
const [countdown, setCountdown] = useState(0)
const [players, setPlayers] = useState([])
const [questionNbr, setQuestionNbr] = useState(0)
const [questionAlreadyDone, setQuestionAlreadyDone] = useState([])
const socketRef = useRef()
const duration = 15
const questionLimit = 10
const { id } = params
const roomNameDisplay = id.substring(0,3) + " " + id.substring(3,6)
useEffect(() => {
const username = localStorage.getItem('name')
setName(username)
// Listen for incoming setMessages
socketRef.current = io("ws://localhost:3000");
socketRef.current.on("connect", () => {
setIsConnected(true)
socketRef.current.emit('room_connect', {"id": id, "name": username})
});
socketRef.current.on("new_player", (params) => {
setPlayers(oldPlayers => [...oldPlayers, params])
})
socketRef.current.on("start_game", (params) => {
setQuestionNbr(oldQ => oldQ + 1)
setGameStarted(true)
setQuestionDisplayed(params.question)
setPossibleChoice(params.possibleChoice)
setCountdown(params.duration)
})
socketRef.current.on("next_question", (params) => {
setQuestionNbr(oldQ => oldQ + 1)
setChoice("")
setQuestionDisplayed(params.question)
setPossibleChoice(params.possibleChoice)
setCountdown(params.duration)
setTotalVotes(0)
})
socketRef.current.on("reset_game", (params) => {
setGameStarted(false)
setCountdown(0)
setPossibleChoice([])
setQuestionDisplayed("")
setChoice("")
setQuestionNbr(0)
setTotalVotes(0)
})
socketRef.current.on("room_joined", (params) => {
setPlayers(params.room_users)
setRole(params.role)
})
// Clean up the socket connection on unmount
return () => {
socketRef.current.disconnect();
};
}, []);
function forceUpdateFunc(){
setForceUpdate(fu => fu + 1)
}
useEffect(() => {
const eventListener = (params) => {
setTotalVotes(oldTotal => oldTotal += 1)
console.log(params)
let temp_possibleChoice = possibleChoice
temp_possibleChoice.forEach((playerChoice) => {
if(playerChoice.name == params.choice) {
playerChoice.nbrVotes += 1
}
})
forceUpdateFunc()
};
socketRef.current.on("player_choice", eventListener)
return () => socketRef.current.off("player_choice", eventListener)
}, [possibleChoice])
useEffect(() => {
countdown > 0 && setTimeout(() => setCountdown(countdown - 1), 1000);
}, [countdown]);
function getPossibleChoice() {
let choice1 = Math.floor(Math.random() * players.length)
console.log(players.length)
let choice2 = Math.floor(Math.random() * players.length)
console.log(choice2)
while(choice2 == choice1){
choice2 = Math.floor(Math.random() * players.length)
}
let playerChoice1 = players[choice1]
playerChoice1.nbrVotes = 0
let playerChoice2 = players[choice2]
playerChoice2.nbrVotes = 0
return [playerChoice1, playerChoice2]
}
function startGame() {
let possibleChoice = getPossibleChoice()
let questionObj = getRandomQuestion()
let question = questionObj.question
setQuestionAlreadyDone(questionObj.alreadyDone)
socketRef.current.emit("start_game", {roomId: id, question: question, possibleChoice: possibleChoice, duration: duration})
setCountdown(duration)
setPossibleChoice(possibleChoice)
setQuestionDisplayed(question)
setGameStarted(true)
setQuestionNbr(oldQ => oldQ + 1)
}
function nextQuestion() {
let possibleChoice = getPossibleChoice()
let questionObj = getRandomQuestion(questionAlreadyDone)
let question = questionObj.question
setQuestionAlreadyDone(questionObj.alreadyDone)
socketRef.current.emit("next_question", {roomId: id, question: question, possibleChoice: possibleChoice, duration: duration})
setCountdown(duration)
setPossibleChoice(possibleChoice)
setQuestionDisplayed(question)
setChoice("")
setQuestionNbr(oldQ => oldQ + 1)
setTotalVotes(0)
}
function resetGame() {
socketRef.current.emit("reset_game", {roomId: id})
setGameStarted(false)
setCountdown(0)
setPossibleChoice([])
setQuestionDisplayed("")
setChoice("")
setQuestionNbr(0)
setTotalVotes(0)
}
function setAndSendChoice(playerName) {
setChoice(playerName)
setTotalVotes(oldTotal => oldTotal += 1)
let temp_possibleChoice = possibleChoice
temp_possibleChoice.forEach((playerChoice) => {
if(playerChoice.name == playerName) {
playerChoice.nbrVotes += 1
}
})
setPossibleChoice(temp_possibleChoice)
socketRef.current.emit("player_choice", {roomId: id, choice: playerName, player: name})
}
return (
<main className="flex min-h-screen flex-col items-center space-y-16 p-4">
{ !gameStarted &&
<>
<div className="flex flex-col">
<h1 className="text-3xl">{roomNameDisplay}</h1>
</div>
<div className="flex flex-col space-y-4">
{ !isConnected &&
<p>Connecting to room...</p>
}
{ (isConnected && players.length == 0) &&
<p>Waiting for players to join...</p>
}
{ (isConnected && players.length > 0) &&
<div className="flex flex-col space-y-16">
<div>
{ players.map((player) => {
console.log(player)
if(player.role == "player") {
return (
<p>{player.name}</p>
)
}
if(player.role == "owner") {
return (
<p className="flex flex-row">{player.name}<IconCrown /></p>
)
}
})}
</div>
{ role == "owner" &&
<button className="btn btn-primary" onClick={startGame}>Start game</button>
}
</div>
}
</div>
</>
}
{ gameStarted &&
<>
<div className="flex flex-col space-y-4 items-center">
<h1 className="text-3xl">{questionDisplayed}</h1>
<span className="indicator-item badge indicator-bottom indicator-center opacity-30">{questionNbr}/{questionLimit}</span>
</div>
{ countdown > 0 &&
<>
<span className="countdown">
<span style={{"--value":countdown.toString()}}></span>
</span>
<div className="flex flex-row space-x-4">
{ choice == "" &&
<>
{ possibleChoice.map((playerChoice) => (
<button className="btn btn-primary" disabled={choice != ""} onClick={() => {setAndSendChoice(playerChoice.name)}}>{playerChoice.name}</button>
))}
</>
}
{ choice != "" &&
<span>Waiting <span className="loading loading-dots loading-xs"></span></span>
}
</div>
</>
}
{ countdown == 0 &&
<>
<div className="flex flex-row space-x-4">
{ possibleChoice.map((playerChoice) => {
console.log(totalVotes)
console.log(playerChoice.nbrVotes / totalVotes)
let percent = Math.floor((playerChoice.nbrVotes / totalVotes) * 100)
return (
<div className="flex items-center space-y-4 flex-col">
<div className="radial-progress text-primary" style={{ "--value": percent }} role="progressbar">
{percent}%
</div>
<h1>{playerChoice.name}</h1>
</div>
)})}
</div>
{ (role == "owner" && questionNbr < questionLimit) &&
<button className="btn btn-primary" onClick={nextQuestion}>Next question</button>
}
{ (role == "owner" && questionNbr == questionLimit) &&
<button className="btn btn-primary" onClick={resetGame}>Replay</button>
}
</>
}
</>
}
</main>
);
}

107
app/[id]/questions.ts Normal file
View file

@ -0,0 +1,107 @@
export const questions = [
//"Qui organiserait le mieux des vacances ?",
//"Qui serait plus susceptible de vivre à Paris",
//"Qui est le/la plus gourmand·e ?",
//"Qui a vote le plus à droite ?",
//"Qui cuisine le mieux ?",
//"Qui pourrait passer son temps à dormir ?",
//"Qui est la plus petite tasse ?",
//"Qui est le/la plus sportif·ve ?",
//"Qui a le meilleur sens de l'humour ?",
//"Qui est le/la plus ponctuel·le ?",
//"Qui est le/la plus aventureux·se ?",
//"Qui cuisine le mieux ?",
//"Qui a le plus de connaissances en musique ?",
//"Qui est le/la plus artistique ?",
//"Qui est le/la plus sociable ?",
//"Qui est le/la plus compétitif·ve ?",
//"Qui est le/la plus susceptible de devenir célèbre ?",
//"Qui est le/la plus généreux·se ?",
//"Qui a le plus de chances de voyager dans l'espace ?",
//"Qui a les meilleures compétences en survie ?",
//"Qui a le plus d'animaux de compagnie ?",
//"Qui est le/la plus organisé·e ?",
//"Qui est le/la plus romantique ?",
//"Qui est le/la plus économe ?",
//"Qui a le plus de style vestimentaire ?",
//"Qui est le/la plus fêtard·e ?",
//"Qui a le plus de talents cachés ?",
//"Qui est le/la plus technophile ?",
//"Qui est le/la plus écolo ?",
//"Qui est le/la plus doué·e pour les langues étrangères ?",
//"Qui a le plus de chances de gagner un marathon ?",
//"Qui est le/la plus passionné·e de lecture ?",
//"Qui est le/la plus doué·e pour le bricolage ?",
//"Qui est le/la plus susceptible de réussir en affaires ?",
//"Qui a le plus de connaissances en cinéma ?",
//"Qui est le/la plus spirituel·le ?",
//"Qui est le/la plus susceptible de vivre à l'étranger ?",
//"Qui est le/la plus drôle ?",
//"Qui est le/la plus susceptible de participer à une émission de télé-réalité ?",
//"Qui a le plus de patience ?",
//"Qui est le/la plus passionné·e de jeux vidéo ?",
//"Qui est le/la plus susceptible de devenir un·e chef·fe renommé·e ?",
//"Qui est le/la plus doué·e pour les jeux de société ?",
//"Qui a le plus de connaissances historiques ?",
//"Qui est le/la plus passionné·e de jardinage ?",
//"Qui a les meilleurs talents d'acteur·rice ?",
"Qui va sauter dans la piscine ?",
"Qui date le plus âgé ?",
"Qui est le plus flexible avec démonstration ?",
"Qui va faire un striptease ?",
"Qui sera la pire parent ?",
"Qui est le plus alcoolique ?",
"Qui serait capable de chier dans la seine ?",
"Qui va montrer son dernier message envoyé ?",
"Qui va faire une roulette de photos ?",
"Qui a déjà bouffé les deux boules d'un mec ?",
"Qui va nous décrire son crush ?",
"Qui est le plus addict ?",
"Qui est le plus croquant ?",
"Qui s'auto-suce le plus ?",
"Qui va prendre une fessée avec la tongue de l'archi Vergnaud ?",
"Qui est le plus à droite ?",
"Qui conduit le mieux ?",
"Qui gagne au bras de fer ? (Preuve)",
"Qui va faire une roulade ?",
"Qui a le plus une tête de con•ne ?",
"Qui est le meilleur menteur ?",
"Qui rikou ?",
"Qui va boire un shot d'huile ?",
"Qui va manger une cuillère a soupe de mayonnaise ?",
"Qui va choisir ce que l'autre va manger ?",
"Qui va nous organiser les prochaines vacances ?",
"Qui a la plus belle fesse ?",
"Qui est le plus intrusif ?",
"Qui a le plus haut bodycount ?",
"Qui va imiter Nicolas Pham ?",
"Qui va dire vinaigrette sans Vi ?",
"Qui va faire son âge divisé par deux en pompes ?",
"Qui va faire l'intro pour le dernier tiktok ?",
"Qui va faire une photo sexy avec son voisin de droite ?",
"Qui va brouter de l'herbe ?",
"Qui va boire un Ricard piscine ?",
"Qui est susceptible de se marier ?",
"À qui donneriez vous vos enfants ?",
"Qui serait le meilleur DDP ?",
"À qui tu te confierais le plus ?",
"Qui est le plus béru ?",
"Qui est le plus kinky ?",
"Qui va nous chanter le menu ?",
"Qui lance un monôme tout nu ?",
"Qui est le plus rapide ?",
"Qui est le plus susceptible d'être célèbre ?",
"Qui est le plus romantique ?",
"Qui va raconter la chose la plus folle de sa vie ?",
"Qui se masturbe le plus ?",
"Qui est le plus gros fan de star wars ?",
"Qui a pleuré en dernier ?",
]
export function getRandomQuestion(alreadyDone = []){
let questionIndex = Math.floor(Math.random() * questions.length)
while(alreadyDone.includes(questionIndex)){
questionIndex = Math.floor(Math.random() * questions.length)
}
return {alreadyDone: [...alreadyDone, questionIndex], question: questions[questionIndex]}
}

7
app/action.ts Normal file
View file

@ -0,0 +1,7 @@
'use server'
import { redirect } from 'next/navigation'
export async function navigate(data: FormData) {
redirect(`/${data.get('id')}`)
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

25
app/globals.css Normal file
View file

@ -0,0 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

22
app/layout.tsx Normal file
View file

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

63
app/page.tsx Normal file
View file

@ -0,0 +1,63 @@
'use client'
import { navigate } from './action'
import { useState, useEffect, useRef } from 'react'
import { IconPencilMinus } from "@tabler/icons-react"
export default function Home() {
const [name, setName] = useState("")
const modal = useRef()
const inputName = useRef()
function setUsername() {
setName(inputName.current.value)
localStorage.setItem("name", inputName.current.value)
}
useEffect(() => {
let localName = localStorage.getItem("name")
if(localName != null){
setName(localName)
} else {
modal.current.showModal()
}
}, [])
return (
<main data-theme="light" className="flex min-h-screen flex-col items-center space-y-16 p-24">
<dialog ref={modal} className="modal">
<div className="modal-box">
<label className="form-control w-full max-w-xs">
<div className="label">
<span className="label-text">What is your name?</span>
</div>
<input type="text" ref={inputName} defaultValue={name} placeholder="Name" className="input input-bordered w-full max-w-xs" />
</label>
<div className="modal-action">
<form method="dialog">
{/* if there is a button in form, it will close the modal */}
<button onClick={setUsername} className="btn">Close</button>
</form>
</div>
</div>
</dialog>
<h1 className="text-3xl">Bazaar</h1>
<div className="flex flex-col space-y-4 items-center">
<div className="flex flex-row space-x-4">
<span className="text-xl">{name}</span>
<button onClick={() => { modal.current.showModal()}}>
<IconPencilMinus />
</button>
</div>
</div>
<div className="flex flex-col space-y-4 items-center">
<a href="/random" className="btn btn-primary">Create a room</a>
<div className="divider"></div>
<form action={navigate} className="flex flex-col space-y-4">
<input type="text" name="id" placeholder="Room pin" className="input input-bordered w-full max-w-xs" />
<button className="btn btn-primary">Join Room</button>
</form>
</div>
</main>
);
}

9
app/random/page.tsx Normal file
View file

@ -0,0 +1,9 @@
import { redirect } from 'next/navigation'
export default function Home() {
let roomNumber = ""
for(let i = 0; i < 6; i++) {
roomNumber += Math.floor(Math.random() * 9.99)
}
redirect('/' + roomNumber.toString())
}

4
next.config.mjs Normal file
View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

5556
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "bazaar",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "node server.mjs",
"build": "next build",
"start": "NODE_ENV='production' && node server.mjs",
"lint": "next lint",
"websocket": "node server.mjs"
},
"dependencies": {
"@tabler/icons-react": "^3.11.0",
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"daisyui": "^4.12.10",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

62
server.mjs Normal file
View file

@ -0,0 +1,62 @@
import { createServer } from "node:http";
import next from "next";
import { Server } from "socket.io";
const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handler = app.getRequestHandler();
let active_rooms = {}
app.prepare().then(() => {
const httpServer = createServer(handler);
const io = new Server(httpServer);
io.on("connection", (socket) => {
console.log("User connected " + socket.id)
socket.on('room_connect', (room) => {
if(!Object.keys(active_rooms).includes(room.id)){
console.log("First person joined " + room.id + " ! " + room.name + " is owner.")
active_rooms[room.id] = [{id: socket.id, name: room.name, role: "owner", vote: ""}]
socket.emit("room_joined", {"room_users": active_rooms[room.id], "role": "owner"})
} else {
socket.to(room.id).emit("new_player",{"id": socket.id, "name": room.name, role: "player"})
active_rooms[room.id].push({id: socket.id, name: room.name, role: "player", vote: ""})
socket.emit("room_joined", {"room_users": active_rooms[room.id], role: "player"})
console.log("New person joined " + room.id + " ! " + room.name + " is player.")
}
socket.join(room.id)
})
socket.on("start_game", (params) => {
socket.to(params.roomId).emit("start_game",{possibleChoice: params.possibleChoice, question: params.question, duration: params.duration})
})
socket.on("reset_game", (params) => {
socket.to(params.roomId).emit("reset_game")
})
socket.on("next_question", (params) => {
socket.to(params.roomId).emit("next_question",{possibleChoice: params.possibleChoice, question: params.question, duration: params.duration})
})
socket.on("player_choice", (params) => {
console.log(params)
socket.to(params.roomId).emit("player_choice", {choice: params.choice, player: params.player})
})
// ...
});
httpServer
.once("error", (err) => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
});

25
tailwind.config.ts Normal file
View file

@ -0,0 +1,25 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [
require('daisyui'),
],
daisyui: {
themes: ["light", "dark", "cupcake"],
},
};
export default config;

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}