diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a8a8b21d --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,36 @@ +# The name of the workflow +name: Frontend Testing + +# This workflow will run on any push to the repository +on: push + +jobs: + test: + name: Frontend Tests + # Similar to docker, we set up a virtual machine to run our tests + runs-on: ubuntu-latest + + steps: + # Each step has a name, some code, and some options + - name: Check out the code + uses: actions/checkout@v3 # This is a reference to some code to run + + # This step installs the Node version + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.x + + # Move to frontend directory and install dependencies + - name: Move to frontend and install dependencies + run: | + cd frontend + npm install + + # Test React app + - name: Move to frontend and Test + run: | + cd frontend + npm run test + + diff --git a/api/app.js b/api/app.js index d73ca037..4774ae9e 100644 --- a/api/app.js +++ b/api/app.js @@ -5,6 +5,7 @@ const cors = require("cors"); const usersRouter = require("./routes/users"); const postsRouter = require("./routes/posts"); const authenticationRouter = require("./routes/authentication"); +const musicRouter = require("./routes/music") const tokenChecker = require("./middleware/tokenChecker"); const app = express(); @@ -21,6 +22,7 @@ app.use(bodyParser.json()); app.use("/users", usersRouter); app.use("/posts", tokenChecker, postsRouter); app.use("/tokens", authenticationRouter); +app.use("/music", musicRouter) // 404 Handler app.use((_req, res) => { diff --git a/api/controllers/music.js b/api/controllers/music.js new file mode 100644 index 00000000..14819b4f --- /dev/null +++ b/api/controllers/music.js @@ -0,0 +1,93 @@ +const getGenres = async (req, res) => { + const requestOptions = { + method: "GET" + } + + const response = await fetch("https://api.deezer.com/genre", requestOptions); + + if (response.status !== 200) { + throw new Error(`Unable to fetch genres. Status: ${response.status}. Headers: ${await response.text()}`); + } + + const data = await response.json(); + res.status(200).json(data.data) +}; + +const getArtistsForGenre = async (req, res) => { + const genreID = req.params.id; + + const requestOptions = { + method: "GET" + } + + const response = await fetch(`https://api.deezer.com/genre/${genreID}/artists`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch artists. Status: ${response.status}. Headers: ${await response.text()}"); + } + + const data = await response.json(); + res.status(200).json(data.data) +}; + +const getTopTracksForArtist = async (req, res) => { + const artistID = req.params.id; + + const requestOptions = { + method: "GET" + } + + const response = await fetch(`https://api.deezer.com/artist/${artistID}/top`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch top tracks for artist"); + } + + const data = await response.json(); + res.status(200).json(data.data) +}; + +const getTrack = async (req, res) => { + const trackID = req.params.id; + + const requestOptions = { + method: "GET" + } + + const response = await fetch(`https://api.deezer.com/track/${trackID}`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch top tracks for artist"); + } + + const data = await response.json(); + res.status(200).json(data) +}; + + +const getAlbumsForArtist = async (req, res) => { + const artistID = req.params.id; + + const requestOptions = { + method: "GET" + } + + const response = await fetch(`https://api.deezer.com/artist/${artistID}/albums`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch albums for artist"); + } + + const data = await response.json(); + res.status(200).json(data.data) +}; + +const MusicController = { + getGenres: getGenres, + getArtistsForGenre: getArtistsForGenre, + getTopTracksForArtist: getTopTracksForArtist, + getTrack: getTrack, + getAlbumsForArtist: getAlbumsForArtist +}; + +module.exports = MusicController \ No newline at end of file diff --git a/api/db/db.js b/api/db/db.js index 15eb79de..c55ed14d 100644 --- a/api/db/db.js +++ b/api/db/db.js @@ -1,20 +1,26 @@ const mongoose = require("mongoose"); const connectToDatabase = async () => { - const mongoDbUrl = process.env.MONGODB_URL; - if (!mongoDbUrl) { - console.error( - "No MongoDB url provided. Make sure there is a MONGODB_URL environment variable set. See the README for more details." - ); - throw new Error("No connection string provided"); - } - - await mongoose.connect(mongoDbUrl); + const uri = `mongodb+srv://${process.env.DB_USER}:${process.env.DB_PASSWORD}@kwizical.xpmi3dw.mongodb.net/?retryWrites=true&w=majority&appName=kwizical`; + const clientOptions = { + serverApi: { version: "1", strict: true, deprecationErrors: true }, + }; - if (process.env.NODE_ENV !== "test") { - console.log("Successfully connected to MongoDB"); + try { + // Create a Mongoose client with a MongoClientOptions object to set the Stable API version + await mongoose.connect(uri, clientOptions); + await mongoose.connection.db.admin().command({ ping: 1 }); + console.log( + "Pinged your deployment. You successfully connected to MongoDB!" + ); + } catch { + console.dir + } finally { + // Ensures that the client will close when you finish/error + await mongoose.disconnect(); } }; module.exports = { connectToDatabase }; + diff --git a/api/index.js b/api/index.js index 3329929b..3175dd57 100644 --- a/api/index.js +++ b/api/index.js @@ -5,7 +5,7 @@ const app = require("./app.js"); const { connectToDatabase } = require("./db/db.js"); const listenForRequests = () => { - const port = process.env.PORT || 3000; + const port = process.env.PORT || 10000; app.listen(port, () => { console.log("Now listening on port", port); }); diff --git a/api/models/post.js b/api/models/post.js index cc64b2fa..645e1192 100644 --- a/api/models/post.js +++ b/api/models/post.js @@ -12,7 +12,7 @@ const Post = mongoose.model("Post", PostSchema); // These lines will create a test post every time the server starts. // You can delete this once you are creating your own posts. -const dateTimeString = new Date().toLocaleString("en-GB"); -new Post({ message: `Test message, created at ${dateTimeString}` }).save(); +// const dateTimeString = new Date().toLocaleString("en-GB"); +// new Post({ message: `Test message, created at ${dateTimeString}` }).save(); module.exports = Post; diff --git a/api/package-lock.json b/api/package-lock.json index 450a9b07..83bbfecd 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1675,10 +1675,13 @@ } }, "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -1712,9 +1715,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -2000,16 +2003,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2040,43 +2043,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2434,9 +2400,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -4297,9 +4263,9 @@ } }, "node_modules/superagent": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", - "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", "dev": true, "dependencies": { "component-emitter": "^1.3.0", @@ -4353,13 +4319,13 @@ "dev": true }, "node_modules/supertest": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", - "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", "dev": true, "dependencies": { "methods": "^1.1.2", - "superagent": "^8.0.5" + "superagent": "^8.1.2" }, "engines": { "node": ">=6.4.0" diff --git a/api/routes/music.js b/api/routes/music.js new file mode 100644 index 00000000..d4c1bd94 --- /dev/null +++ b/api/routes/music.js @@ -0,0 +1,12 @@ +const express = require("express"); +const router = express.Router(); + +const MusicController = require("../controllers/music"); + +router.get("/genre", MusicController.getGenres); +router.get("/genre/:id/artists", MusicController.getArtistsForGenre); +router.get("/artist/:id/top", MusicController.getTopTracksForArtist); +router.get("/track/:id", MusicController.getTrack); +router.get("/artist/:id/albums", MusicController.getAlbumsForArtist); + +module.exports = router; diff --git a/api/tests/controllers/music.test.js b/api/tests/controllers/music.test.js new file mode 100644 index 00000000..24efdf90 --- /dev/null +++ b/api/tests/controllers/music.test.js @@ -0,0 +1,16 @@ +// const request = require("supertest"); +// const app = require("../../app"); + +// TODO: Tests to be added. Need to research mocking fetch with Jest +// test below actually calls Deezer API +// - Aakash / Katrina + +describe("GET, genres", () => { + test("the response code is 200", async () => { + + // const response = await request(app) + // .get("/music/genre") + + // expect(response.status).toEqual(200); + expect(true) +})}); \ No newline at end of file diff --git a/frontend/helpers/album_generator.js b/frontend/helpers/album_generator.js new file mode 100644 index 00000000..c0d51864 --- /dev/null +++ b/frontend/helpers/album_generator.js @@ -0,0 +1,22 @@ +import { shuffle } from "../helpers/shuffle.js"; +import { getAlbumsForArtist } from "../src/services/deezerService.js"; + +export const randomAlbums = async (artistID) => { + try { + + const data = await getAlbumsForArtist(artistID); + //create a list of dictionaries with artist ID and name + const albumList = data.map(album => ({ + id: album.id, + title: album.title + })); + + const shuffledAlbums = shuffle(albumList); // Shuffle the array of albums + const selectedAlbums = shuffledAlbums.slice(0, 4); // Select the first 4 albums + return selectedAlbums; + + } catch (error) { + console.error("Error fetching artists:", error); + throw new Error("Unable to fetch random albums"); + } +}; diff --git a/frontend/helpers/answer_generator.js b/frontend/helpers/answer_generator.js new file mode 100644 index 00000000..6b81d0a8 --- /dev/null +++ b/frontend/helpers/answer_generator.js @@ -0,0 +1,48 @@ +import { randomArtists } from "./artist_generator.js"; +import { randomTrack } from "./track_generator.js"; +import { randomAlbums } from "./album_generator.js" +import { shuffle } from "../helpers/shuffle.js" + + +export const answers = async (genreID) => { + let fourAlbums = false; + let correctAnswerArtistID; + let answerAlbums; + while (!fourAlbums) { + const selectedArtists = await randomArtists(genreID) + correctAnswerArtistID = selectedArtists[0]["id"]; + answerAlbums = await randomAlbums(correctAnswerArtistID); + if (answerAlbums.length >= 4) { + fourAlbums = true; + } + } + const answerList = []; + // Randomly decide whether to use track titles or artist names or album title + // Generates a random number between 0 and 2 + const questionType = Math.floor(Math.random() * 3); + + const { selectedTrack, shuffledTracks } = await randomTrack(correctAnswerArtistID); + if (questionType === 0) { + answerList.push(selectedTrack.title); + for (let i = 1; i < shuffledTracks.length; i++) { + answerList.push(shuffledTracks[i].title); + } + } else if (questionType === 1) { + const answerArtists = await randomArtists(genreID); + answerList.push(selectedTrack.artist); + // Iterate over all artist expect the first and push their names to the answer list + for (let i = 1; i < answerArtists.length; i++) { + answerList.push(answerArtists[i].name); + } + } else { + answerList.push(selectedTrack.album); + for (let i = 1; i < answerAlbums.length; i++) { + answerList.push(answerAlbums[i].title); + } + } + // Shuffle the answers to randomize the position of the right answer + const shuffledArtistAnswerList = shuffle(answerList); + + return { selectedTrack, shuffledArtistAnswerList, questionType }; + +}; diff --git a/frontend/helpers/artist_generator.js b/frontend/helpers/artist_generator.js new file mode 100644 index 00000000..4835dcb1 --- /dev/null +++ b/frontend/helpers/artist_generator.js @@ -0,0 +1,23 @@ +import { shuffle } from "../helpers/shuffle.js"; +import { getArtistsForGenre } from "../src/services/deezerService.js"; + +export const randomArtists = async (genreID) => { + try { + + const data = await getArtistsForGenre(genreID); + //create a list of dictionaries with artist ID and name + const artistList = data.map(artist => ({ + name: artist.name, + id: artist.id + })); + + const shuffledArtists = shuffle(artistList); // Shuffle the array of artists + const selectedArtists = shuffledArtists.slice(0, 4); // Select the first 4 artists + return selectedArtists; + + } catch (error) { + console.error("Error fetching artists:", error); + throw new Error("Unable to fetch random artists"); + } +}; + diff --git a/frontend/helpers/shuffle.js b/frontend/helpers/shuffle.js new file mode 100644 index 00000000..7a76c629 --- /dev/null +++ b/frontend/helpers/shuffle.js @@ -0,0 +1,9 @@ +function shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + + export {shuffle} \ No newline at end of file diff --git a/frontend/helpers/track_generator.js b/frontend/helpers/track_generator.js new file mode 100644 index 00000000..78a1d331 --- /dev/null +++ b/frontend/helpers/track_generator.js @@ -0,0 +1,25 @@ +import { shuffle } from "../helpers/shuffle.js"; +import { getTopTracksForArtist } from "../src/services/deezerService.js"; + +export const randomTrack = async (artistID) => { + try { + const data = await getTopTracksForArtist(artistID); + + const trackList = data.map(track => ({ + id: track.id, + title: track.title, + artist: track.artist.name, + album: track.album.title, + preview: track.preview + })); + + const shuffledTracksList = shuffle(trackList); // Shuffle the array of IDs + const shuffledTracks = shuffledTracksList.slice(0, 4); + const selectedTrack = shuffledTracks[0]; // Select the first track + + return { selectedTrack, shuffledTracks }; + } catch (error) { + console.error("Error fetching tracks:", error); + throw new Error("Unable to fetch random track"); + } +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d4d4ab9e..5fc16357 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,17 @@ "name": "mern-template-frontend", "version": "0.0.0", "dependencies": { + "@react-oauth/google": "^0.12.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.14.2" + "react-router-dom": "^6.14.2", + "tailwind-animatecss": "^1.0.0", + "tailwindcss": "^3.4.3" }, "devDependencies": { + "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", "@types/react": "^18.2.15", @@ -38,6 +44,23 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "dev": true + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.10", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", @@ -669,6 +692,47 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jest/schemas": { "version": "29.6.0", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", @@ -681,17 +745,53 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -704,7 +804,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -713,7 +812,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -722,6 +820,24 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-oauth/google": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", + "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remix-run/router": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", @@ -949,6 +1065,70 @@ "node": ">=14" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", + "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, "node_modules/@testing-library/react": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", @@ -1455,11 +1635,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/animate.css": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz", + "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==", + "peer": true + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1468,7 +1653,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1479,6 +1663,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1619,6 +1825,42 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1634,8 +1876,18 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -1651,7 +1903,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1659,6 +1910,37 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1690,6 +1972,33 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -1733,11 +2042,44 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1748,8 +2090,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1763,6 +2104,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1782,7 +2131,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1792,6 +2140,23 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", @@ -1919,6 +2284,11 @@ "node": ">=0.4.0" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, "node_modules/diff-sequences": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", @@ -1940,6 +2310,11 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1970,6 +2345,21 @@ "node": ">=12" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.749", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.749.tgz", + "integrity": "sha512-LRMMrM9ITOvue0PoBrvNIraVmuDbJV5QC9ierz/z5VilMdPOVMjOtpICNld3PuXuTZ3CHH/UPxX9gHhAPwi+0Q==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2132,6 +2522,14 @@ "@esbuild/win32-x64": "0.18.17" } }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2382,7 +2780,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2398,7 +2795,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -2422,7 +2818,6 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -2443,7 +2838,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2495,6 +2889,21 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -2509,6 +2918,18 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2519,7 +2940,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2532,8 +2952,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -2626,7 +3045,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -2706,7 +3124,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -2868,7 +3285,16 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", @@ -2940,6 +3366,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -2972,7 +3409,6 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -2999,16 +3435,22 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3041,7 +3483,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -3201,8 +3642,32 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "bin": { + "jiti": "bin/jiti.js" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -3309,6 +3774,19 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, "node_modules/local-pkg": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", @@ -3336,6 +3814,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3399,7 +3883,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -3408,7 +3891,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -3438,6 +3920,15 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3450,6 +3941,14 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mlly": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", @@ -3468,11 +3967,20 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -3534,6 +4042,27 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", @@ -3544,11 +4073,18 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -3764,7 +4300,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -3772,8 +4307,30 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.1.tgz", + "integrity": "sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA==", + "engines": { + "node": "14 || >=16.14" + } }, "node_modules/path-type": { "version": "4.0.0", @@ -3802,14 +4359,12 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -3817,6 +4372,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", @@ -3829,10 +4400,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", - "dev": true, + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -3850,12 +4420,142 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3933,7 +4633,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -4008,6 +4707,38 @@ "react-dom": ">=16.8" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", @@ -4067,7 +4798,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -4114,7 +4844,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -4204,7 +4933,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4216,7 +4944,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -4241,6 +4968,17 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4251,10 +4989,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -4283,6 +5020,66 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -4351,7 +5148,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4359,6 +5155,30 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4383,6 +5203,70 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4399,7 +5283,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4413,12 +5296,113 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tailwind-animatecss": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tailwind-animatecss/-/tailwind-animatecss-1.0.0.tgz", + "integrity": "sha512-CHoRvMMR6aTv2fS1A/i6DA8LJkOwzeP0l96sdzuIHu/g79BsGuqQwjOoZS9edFYlJaHZiIwGdmxblm6DdJVK4w==", + "dependencies": { + "postcss-js": "^3.0.3", + "postcss-selector-parser": "^6.0.8" + }, + "peerDependencies": { + "animate.css": "^4", + "postcss": "^8", + "tailwindcss": "^3" + } + }, + "node_modules/tailwind-animatecss/node_modules/postcss-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", + "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dependencies": { + "camelcase-css": "^2.0.1", + "postcss": "^8.1.6" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinybench": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", @@ -4447,7 +5431,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -4494,6 +5477,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4636,6 +5624,35 @@ "node": ">= 4.0.0" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4655,10 +5672,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -4884,7 +5906,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -4961,6 +5982,93 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5009,6 +6117,17 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 697a6a90..89f21942 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,11 +11,17 @@ "test": "vitest" }, "dependencies": { + "@react-oauth/google": "^0.12.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.14.2" + "react-router-dom": "^6.14.2", + "tailwind-animatecss": "^1.0.0", + "tailwindcss": "^3.4.3" }, "devDependencies": { + "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", "@types/react": "^18.2.15", diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 42a627bc..4ce7cda5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,8 @@ import { HomePage } from "./pages/Home/HomePage"; import { LoginPage } from "./pages/Login/LoginPage"; import { SignupPage } from "./pages/Signup/SignupPage"; import { FeedPage } from "./pages/Feed/FeedPage"; +import { QuizPage } from "./pages/Quiz/QuizPage"; +import { ScorePage } from "./pages/Score/ScorePage"; // docs: https://reactrouter.com/en/main/start/overview const router = createBrowserRouter([ @@ -12,6 +14,14 @@ const router = createBrowserRouter([ path: "/", element: , }, + { + path: "/kwizical", + element: , + }, + { + path: "/score", + element: , + }, { path: "/login", element: , diff --git a/frontend/src/components/Answer/Answer.jsx b/frontend/src/components/Answer/Answer.jsx new file mode 100644 index 00000000..10b9daca --- /dev/null +++ b/frontend/src/components/Answer/Answer.jsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from "react"; + +const Answer = ({ + selectedTrack, + shuffledArtistAnswerList, + onAnswerButtonClick, + interactionDisabled, + time +}) => { + const [score, setScore] = useState(0); + const [bonus, setBonus] = useState(0); + const [buttonColors, setButtonColors] = useState( + new Array(4).fill("bg-box-color") + ); + + useEffect(() => { + const storedScore = localStorage.getItem("score"); //add score to localStorage to pass it to ScorePage + if (storedScore) { + setScore(parseInt(storedScore)); + } + const storedBonus = localStorage.getItem("bonus"); + if (storedBonus) { + setBonus(parseInt(storedBonus)); + } + }, []); + + useEffect(() => { + localStorage.setItem("score", score.toString()); + }, [score]); + + useEffect(() => { + localStorage.setItem("bonus", bonus.toString()); + }, [bonus]); + + const answerClick = (answer, id) => { + const isCorrect = (selectedTrack.title === answer) || (selectedTrack.artist === answer) || (selectedTrack.album === answer); + const newButtonColors = [...buttonColors]; + if (isCorrect) { + setScore(score + 100); + if (time < 5) { + setBonus(bonus + 50); // Add bonus points only if the answer is correct and the timer is less than 5 // Save bonus to localStorage + } + + newButtonColors[id] = "bg-correct-color"; + } else { + newButtonColors[id] = "bg-incorrect-color"; + } + setButtonColors(newButtonColors); + onAnswerButtonClick(); + + setTimeout(() => { + setButtonColors(new Array(4).fill("bg-box-color")); + }, 1600); + }; + + return ( + <> +
+ {" "} + {/* 'grid grid-cols-2' this turns the row of answers into two columns. Adding md: applies the changes only when the screen is wider than the md breakpoint*/} + {shuffledArtistAnswerList.map((answer, id) => ( + + ))} +
+
+

+ Results +

+

Your Score: {score}

+ {buttonColors.includes("bg-correct-color") && ( +
+

Correct!

+
+ )} + {buttonColors.includes("bg-incorrect-color") && ( +
+

Wrong!

+
+ )} +

Speed Bonus: {bonus}

+
+ + ); +}; + +export default Answer; diff --git a/frontend/src/components/AudioButton/AudioButton.css b/frontend/src/components/AudioButton/AudioButton.css new file mode 100644 index 00000000..76f7d31d --- /dev/null +++ b/frontend/src/components/AudioButton/AudioButton.css @@ -0,0 +1,35 @@ +.audio .play-button { + width: 250px; + height: 250px; + background-color: #e1434b; + border-radius: 100%; + border: #fff solid 2px; + position: relative; +} + +.audio .play-button.pulsate { + animation: shadowPulse 1s infinite linear; +} + +.icon { + font-size: 100px; + color: white; +} + +@keyframes shadowPulse { + 0% { + box-shadow: 0 0 8px 6px transparent, 0 0 0 0 transparent, + 0 0 0 0 transparent; + } + + 10% { + box-shadow: 0 0 8px 6px #e1434b, 0 0 12px 10px transparent, + 0 0 12px 5px #e1434b; + } + + 80%, + 100% { + box-shadow: 0 0 8px 6px transparent, 0 0 0 40px transparent, + 0 0 0 40px transparent; + } +} diff --git a/frontend/src/components/AudioButton/AudioButton.jsx b/frontend/src/components/AudioButton/AudioButton.jsx new file mode 100644 index 00000000..617daae9 --- /dev/null +++ b/frontend/src/components/AudioButton/AudioButton.jsx @@ -0,0 +1,38 @@ +import { useEffect } from "react"; +import "./AudioButton.css"; + +const AudioButton = ({ trackPreview, onPlayPause, playButtonState }) => { + console.log("Track Preview URL:", trackPreview); + + useEffect(() => { + onPlayPause(false); + }, [trackPreview, onPlayPause]); + + const handleClick = () => { + onPlayPause(!playButtonState); + playPause(); + }; + + function playPause() { + var myAudio = document.getElementById("ASong"); + if (myAudio.paused) { + myAudio.play(); + } else { + myAudio.pause(); + } + } + + return ( +
+ +
+ ); +}; + +export default AudioButton; diff --git a/frontend/src/components/GenrePicker/GenrePicker.jsx b/frontend/src/components/GenrePicker/GenrePicker.jsx new file mode 100644 index 00000000..215aeb5e --- /dev/null +++ b/frontend/src/components/GenrePicker/GenrePicker.jsx @@ -0,0 +1,35 @@ +const GenrePicker = ({ onGenreSelect }) => { + const genres = [ + { id: 132, name: 'Pop', background: 'bg-pop' }, + { id: 116, name: 'Rap/Hiphop', background: 'bg-hiphop' }, + { id: 165, name: 'RnB', background: 'bg-RnB' }, + { id: 113, name: 'Dance', background: 'bg-dance' }, + { id: 173, name: 'Films/Games', background: 'bg-film' }, + { id: 464, name: 'Metal', background: 'bg-metal' } + ] + + + + return ( +
+
+

+ Select a genre +

+
+
+ {genres.map(genre => ( + + ))} +
+
+ ) +} + +export default GenrePicker \ No newline at end of file diff --git a/frontend/src/components/GoogleAuth/GoogleAuth.css b/frontend/src/components/GoogleAuth/GoogleAuth.css new file mode 100644 index 00000000..79248087 --- /dev/null +++ b/frontend/src/components/GoogleAuth/GoogleAuth.css @@ -0,0 +1,34 @@ +.login-with-google-btn { + transition: background-color .3s, box-shadow .3s; + + padding: 12px 16px 12px 42px; + border: none; + border-radius: 3px; + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 1px 1px rgba(0, 0, 0, .25); + + color: #757575; + font-size: 14px; + font-weight: 500; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTcuNiA5LjJsLS4xLTEuOEg5djMuNGg0LjhDMTMuNiAxMiAxMyAxMyAxMiAxMy42djIuMmgzYTguOCA4LjggMCAwIDAgMi42LTYuNnoiIGZpbGw9IiM0Mjg1RjQiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxwYXRoIGQ9Ik05IDE4YzIuNCAwIDQuNS0uOCA2LTIuMmwtMy0yLjJhNS40IDUuNCAwIDAgMS04LTIuOUgxVjEzYTkgOSAwIDAgMCA4IDV6IiBmaWxsPSIjMzRBODUzIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNNCAxMC43YTUuNCA1LjQgMCAwIDEgMC0zLjRWNUgxYTkgOSAwIDAgMCAwIDhsMy0yLjN6IiBmaWxsPSIjRkJCQzA1IiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNOSAzLjZjMS4zIDAgMi41LjQgMy40IDEuM0wxNSAyLjNBOSA5IDAgMCAwIDEgNWwzIDIuNGE1LjQgNS40IDAgMCAxIDUtMy43eiIgZmlsbD0iI0VBNDMzNSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PHBhdGggZD0iTTAgMGgxOHYxOEgweiIvPjwvZz48L3N2Zz4=); + background-color: white; + background-repeat: no-repeat; + background-position: 12px 11px; + + &:hover { + box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 2px 4px rgba(0, 0, 0, .25); + } + + &:active { + background-color: #eeeeee; + } + + &:focus { + outline: none; + box-shadow: + 0 -1px 0 rgba(0, 0, 0, .04), + 0 2px 4px rgba(0, 0, 0, .25), + 0 0 0 3px #c8dafc; + } +} \ No newline at end of file diff --git a/frontend/src/components/GoogleAuth/GoogleAuth.jsx b/frontend/src/components/GoogleAuth/GoogleAuth.jsx new file mode 100644 index 00000000..946e9908 --- /dev/null +++ b/frontend/src/components/GoogleAuth/GoogleAuth.jsx @@ -0,0 +1,28 @@ +import { useGoogleLogin } from '@react-oauth/google'; +import { useNavigate } from 'react-router-dom'; +import "./GoogleAuth.css" + +const GoogleAuth = () => { + const navigate = useNavigate() + const login = useGoogleLogin({ + onSuccess: (tokenResponse) => { + localStorage.setItem("google-token", tokenResponse.access_token) + fetch('https://www.googleapis.com/oauth2/v3/userinfo', { + headers: { Authorization: `Bearer ${tokenResponse.access_token}` }, + }) + .then(res => res.json()) + .then(data => { + localStorage.setItem("user", data) + navigate("/kwizical") + }); + }, + onError: (error) => console.log('Login Failed:', error) + }); + + return ( +
+ +
) +} + +export default GoogleAuth \ No newline at end of file diff --git a/frontend/src/components/Question/Question.jsx b/frontend/src/components/Question/Question.jsx new file mode 100644 index 00000000..67a3ce0b --- /dev/null +++ b/frontend/src/components/Question/Question.jsx @@ -0,0 +1,24 @@ + +const Question = ({ questionType, hidden }) => { + + const whichQuestion = (questionType) => { + console.log(questionType) + if (questionType === 0) { + return "What is the name of the track?" + } else if (questionType === 1) { + return "What is the name of the artist?" + } else { + return "What is the name of the album?" + } + } + return ( + <> + + + ); +}; + +export default Question; diff --git a/frontend/src/components/Timer/timer.jsx b/frontend/src/components/Timer/timer.jsx new file mode 100644 index 00000000..8f0ad26b --- /dev/null +++ b/frontend/src/components/Timer/timer.jsx @@ -0,0 +1,28 @@ +import { useState, useEffect } from "react"; + + + + +export const Timer = ({onTimerUpdate }) => { + const [isPlaying, setIsPlaying] = useState (false) + + + const handlePlayPause = () => { + setIsPlaying(!isPlaying); + //When play is pressed, sets to isplaying. When pressed again, sets to !isplaying + + } + + useEffect(() => { + let interval; + if (isPlaying) { + interval = setInterval(() => { + onTimerUpdate(prevTimer => prevTimer + 1); + }, 1000); + } + return () => clearInterval(interval); + }, [isPlaying, onTimerUpdate]); + + return null; // This component doesn't render anything visible +}; + diff --git a/frontend/src/index.css b/frontend/src/index.css index 9bd6cfed..8a1e2fe8 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,5 +1,10 @@ -:root { +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + } a { @@ -22,4 +27,5 @@ body { h1 { font-size: 3.2em; line-height: 1.1; -} +} */ + diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b69c8d5e..bcf15449 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,15 +1,19 @@ import ReactDOM from "react-dom/client"; import React from "react"; - +import 'tailwindcss/tailwind.css'; import App from "./App.jsx"; import "./index.css"; +import { GoogleOAuthProvider } from '@react-oauth/google'; + // Get the "root" div from index.html. // The React application will be inserted into this div. const rootElement = document.getElementById("root"); ReactDOM.createRoot(rootElement).render( - - - + + + + + ); diff --git a/frontend/src/pages/Home/HomePage.jsx b/frontend/src/pages/Home/HomePage.jsx index 39f95875..7e6ce7f5 100644 --- a/frontend/src/pages/Home/HomePage.jsx +++ b/frontend/src/pages/Home/HomePage.jsx @@ -1,13 +1,19 @@ import { Link } from "react-router-dom"; +import GoogleAuth from "../../components/GoogleAuth/GoogleAuth"; import "./HomePage.css"; + + export const HomePage = () => { return (
-

Welcome to Acebook!

- Sign Up - Log In +

{"Let's get Kwizical!"}

+
+ +
+
or
+ Play as guest
); }; diff --git a/frontend/src/pages/Quiz/QuizPage.jsx b/frontend/src/pages/Quiz/QuizPage.jsx new file mode 100644 index 00000000..88c3ce45 --- /dev/null +++ b/frontend/src/pages/Quiz/QuizPage.jsx @@ -0,0 +1,134 @@ +import { useState, useEffect, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import AudioButton from "../../components/AudioButton/AudioButton"; +import Question from "../../components/Question/Question"; +import Answer from "../../components/Answer/Answer"; +import { answers } from "../../../helpers/answer_generator"; +import GenrePicker from "../../components/GenrePicker/GenrePicker"; + +export const QuizPage = () => { + const [shuffledArtistAnswerList, setShuffledArtistAnswerList] = useState([]); + const [selectedTrack, setSelectedTrack] = useState(""); + const [selectedGenre, setSelectedGenre] = useState(0); + const [time, setTime] = useState(0); + const [playButtonState, setPlayButtonState] = useState(false); + const [selectedBackground, setSelectedBackground] = + useState("custom-background"); + const [questionType, setQuestionType] = useState(null) + const [questionNumber, setQuestionNumber] = useState(1); + const navigate = useNavigate(); + const [interactionDisabled, setInteractionDisabled] = useState(false); + const [animate, setAnimate] = useState(false); + + const handleAnswerButtonClick = () => { + setInteractionDisabled(true); + setTimeout(() => { + setQuestionNumber(questionNumber + 1); + }, 1000); + }; + + + useEffect(() => { + setAnimate(false); + + if (questionNumber <= 5) { + answers(selectedGenre).then( + ({ selectedTrack, shuffledArtistAnswerList, questionType }) => { + setShuffledArtistAnswerList(shuffledArtistAnswerList); + setSelectedTrack(selectedTrack); + setQuestionType(questionType); + setInteractionDisabled(false); + setAnimate(true); + } + ); + } else { + setTimeout(() => { + navigate("/score"); + }, 750); + } + }, [selectedGenre, questionNumber, navigate]); + + +const handlePlayPause = useCallback((newState) => { + setPlayButtonState(newState); + //When play is pressed, sets to isplaying. When pressed again, sets to !isplaying + }, []); + + useEffect(() => { + let interval; + if (playButtonState) { + interval = setInterval(() => { + setTime((prevTimer) => { + console.log("Timer updated:", prevTimer + 1); // Log the updated timer value + return prevTimer + 1; + }); + }, 1000); + } + return () => clearInterval(interval); + }, [playButtonState]); + + const handleGenrePicker = (genreID, backgroundClass) => { + setSelectedGenre(genreID); + setSelectedBackground(backgroundClass); + localStorage.setItem('genreID', genreID); //Adding genreID to LocalStorage to get it in ScorePage + + }; + + return ( + <> +
+ {selectedGenre === 0 ? ( +
+ +
+ ) : ( + <> +
+
+ +
+ +
+
+ +
+
+ +
+
+
+ + )} +
+ + ); +}; \ No newline at end of file diff --git a/frontend/src/pages/Score/ScorePage.jsx b/frontend/src/pages/Score/ScorePage.jsx new file mode 100644 index 00000000..5c11b6e1 --- /dev/null +++ b/frontend/src/pages/Score/ScorePage.jsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export const ScorePage = () => { + const [score, setScore] = useState(null); + const [genreID, setGenreId] = useState(null); + const [bonus, setBonus] = useState(null); + const [perfectRoundBonus, setPerfectRoundBonus] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + const genreID = localStorage.getItem("genreID"); + if (genreID) { + setGenreId(parseInt(genreID)); + } + const storedScore = localStorage.getItem("score"); + if (storedScore) { + setScore(parseInt(storedScore)); + } + const storedBonus = localStorage.getItem("bonus"); + if (storedBonus) { + setBonus(parseInt(storedBonus)); + } + }, []); + + useEffect(() => { + if (score === 500) { + setPerfectRoundBonus(250); + } else { + setPerfectRoundBonus(0); + } + }, [score]); + + const getRating = (score, genreID) => { + if (score <= 200) { + switch (genreID) { + case 116: // Hip Hop + return "Do you even know where Brooklyn is?"; + case 113: // Dance + return "Do you even move your feet?"; + case 132: // Pop + return "Do you even moonwalk?"; + case 165: // RnB + return "You can't even slow dance!"; + case 173: // Films/Games + return "Do you even have a TV?"; + case 464: // Metal + return "You're clearly a groupie"; + default: + return "Default rating message"; + } + } else if (score >= 300 && score < 500) { + switch (genreID) { + case 116: // Hip Hop + return "Not bad 25 Cent"; + case 113: // Dance + return "You can do better two stepper!"; + case 132: // Pop + return "You can do better moonwalks!"; + case 165: // RnB + return "Keep singing in the shower"; + case 173: // Films/Games + return "TV's nowadays have colors"; + case 464: // Metal + return "Not bad, but you're out of tune!"; + default: + return "Default rating message"; + } + } else if (score === 500) { + switch (genreID) { + case 116: // Hip Hop + return "Keep spitting those bars!"; + case 113: // Dance + return "Great! You got the moves!"; + case 132: // Pop + return "Oh yeah! It's Britney b*$^&!"; + case 165: // RnB + return "You're hitting the right keys Alicia!"; + case 173: // Films/Games + return "You wrote the script for success!"; + case 464: // Metal + return "You're a true Metal Head!"; + default: + return "Default rating message"; + } + } + } + +const totalScore = score + bonus + perfectRoundBonus; + +const rating = getRating(score, genreID); + +const handleGoBack = () => { + navigate("/kwizical"); +}; + +return ( +
+

Total Score Breakdown

+

Round Score: {score}

+

Perfect Round Bonus: {perfectRoundBonus}

+

Speed Bonus: {bonus}

+

Your Score: {totalScore}

+ +

Rating: {rating}

+
+ +
+
+); +}; + +export default ScorePage; \ No newline at end of file diff --git a/frontend/src/services/deezerService.js b/frontend/src/services/deezerService.js new file mode 100644 index 00000000..f551e3f6 --- /dev/null +++ b/frontend/src/services/deezerService.js @@ -0,0 +1,79 @@ +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL; + +export const getGenres = async () => { + const requestOptions = { + method: "GET", + }; + + const response = await fetch(`${BACKEND_URL}/music/genre`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch genres"); + } + + const data = await response.json(); + return data; +}; + +export const getArtistsForGenre = async (genreID) => { + const requestOptions = { + method: "GET" + }; + + const response = await fetch(`${BACKEND_URL}/music/genre/${genreID}/artists`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch artists"); + } + + const data = await response.json(); + return data; +}; + +export const getTopTracksForArtist = async (artistID) => { + const requestOptions = { + method: "GET" + }; + + const response = await fetch(`${BACKEND_URL}/music/artist/${artistID}/top`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch top tracks for artist"); + } + + const data = await response.json(); + return data; +}; + +export const getTrack = async (trackID) => { + const requestOptions = { + method: "GET" + }; + + const response = await fetch(`${BACKEND_URL}/music/track/${trackID}`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch track"); + } + + const data = await response.json(); + return data; +}; + + +export const getAlbumsForArtist = async (artistID) => { + const requestOptions = { + method: "GET" + }; + + const response = await fetch(`${BACKEND_URL}/music/artist/${artistID}/albums`, requestOptions); + + if (response.status !== 200) { + throw new Error("Unable to fetch artists"); + } + + const data = await response.json(); + return data; +}; + + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 00000000..2ed7cdea --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,65 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ "./index.html", + "./src/**/*.{js,ts,jsx,tsx}",], + theme: { + + extend: { + backgroundColor: { + 'box-color': '#F8F8F6', // Define the custom box color - currently cream + 'hover-color': '#2B2939', // currently navy + 'correct-color': '#38a169', //equivalent to green-500 + 'incorrect-color': '#e53e3e' //equivalent to red-500 + }, + textColor: { + 'title-color': '#2B2939', // currently navy + 'text-color': '#2B2939', // currently navy + 'hover-text-color':'#F8F8F6', // currently cream + 'question-text-color':'#F8F8F6', // currently cream + }, + backgroundImage: { + 'metal': "url(https://media.istockphoto.com/id/1468692377/photo/red-hot-scorching-magma-approaching-the-gate-to-the-dark-hell-from-both-sides.jpg?s=612x612&w=0&k=20&c=0uEKtBMNgFj_Atbn2Y463q2Q0BxBP3p6VCWXnrgTm5M=)", + 'RnB': "url(https://images.unsplash.com/photo-1500462918059-b1a0cb512f1d?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)", + 'dance': "url(https://images.unsplash.com/photo-1578736641330-3155e606cd40?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDd8fHBvcCUyMG11c2ljfGVufDB8fDB8fHww)", + 'hiphop': "url(https://images.unsplash.com/photo-1546528377-9049abbac32f?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGdyYWZmaXRpJTIwaGlwJTIwaG9wfGVufDB8fDB8fHww)", + 'pop': "url(https://images.unsplash.com/photo-1529245856630-f4853233d2ea?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8cG9wJTIwbXVzaWN8ZW58MHx8MHx8fDA%3D)", + 'film': "url(https://images.unsplash.com/photo-1490971588422-52f6262a237a?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8ZmlsbXMlMjBnYW1lcyUyMGFic3RyYWN0fGVufDB8fDB8fHww)" + }, + fontFamily: { + 'font-metal': ['Jacquard', 'sans-serif'], + }, + }, + }, + plugins: [ + // The following plugins provides the sliding transition animation between "components" on the QuizPage + // Initially being used to transition between the genrepicker component and the question and answer components (that make up the quiz) + + // Below we are defining an anonymous function with addUtilities as an object + // addUtilities is a Tailwind function that is used to register new utility classes + ({ addUtilities }) => { + + addUtilities({ + // here we are defining the custom utilities + '@keyframes slideInRight': { + from: { transform: 'translateX(100%)'}, // This defines that the element starts fully to the right + to: { transform: 'translateX(0)' }, // This defines that the element ends in it's original position + }, + '.animate__slideInRight': { + animation: 'slideInRight', // Sets the slideInRight animation from the Animate.css library + animationDuration: `1000ms` + }, + + '.custom-background': { + backgroundColor: 'linear-gradient(to bottom right, #FCD34D, #F59E0B)', + backgroundImage: 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'100%25\' height=\'100%25\' viewBox=\'0 0 1600 800\'%3E%3Cg stroke=\'%23D34128\' stroke-width=\'63.8\' stroke-opacity=\'0.21\' %3E%3Ccircle fill=\'%23C79F7B\' cx=\'0\' cy=\'0\' r=\'1800\'/%3E%3Ccircle fill=\'%23c89d7c\' cx=\'0\' cy=\'0\' r=\'1700\'/%3E%3Ccircle fill=\'%23c89c7d\' cx=\'0\' cy=\'0\' r=\'1600\'/%3E%3Ccircle fill=\'%23c99a7e\' cx=\'0\' cy=\'0\' r=\'1500\'/%3E%3Ccircle fill=\'%23ca987f\' cx=\'0\' cy=\'0\' r=\'1400\'/%3E%3Ccircle fill=\'%23cb9680\' cx=\'0\' cy=\'0\' r=\'1300\'/%3E%3Ccircle fill=\'%23cb9481\' cx=\'0\' cy=\'0\' r=\'1200\'/%3E%3Ccircle fill=\'%23cc9382\' cx=\'0\' cy=\'0\' r=\'1100\'/%3E%3Ccircle fill=\'%23cd9183\' cx=\'0\' cy=\'0\' r=\'1000\'/%3E%3Ccircle fill=\'%23cd8f84\' cx=\'0\' cy=\'0\' r=\'900\'/%3E%3Ccircle fill=\'%23ce8d85\' cx=\'0\' cy=\'0\' r=\'800\'/%3E%3Ccircle fill=\'%23cf8b86\' cx=\'0\' cy=\'0\' r=\'700\'/%3E%3Ccircle fill=\'%23cf8a87\' cx=\'0\' cy=\'0\' r=\'600\'/%3E%3Ccircle fill=\'%23d08888\' cx=\'0\' cy=\'0\' r=\'500\'/%3E%3Ccircle fill=\'%23d08689\' cx=\'0\' cy=\'0\' r=\'400\'/%3E%3Ccircle fill=\'%23d1848a\' cx=\'0\' cy=\'0\' r=\'300\'/%3E%3Ccircle fill=\'%23d1828b\' cx=\'0\' cy=\'0\' r=\'200\'/%3E%3Ccircle fill=\'%23D2808C\' cx=\'0\' cy=\'0\' r=\'100\'/%3E%3C/g%3E%3C/svg%3E")', + + + }, + //https://www.svgbackgrounds.com/set/free-svg-backgrounds-and-patterns/ + //https://heropatterns.com + + }); + }, + ], +} + diff --git a/frontend/tests/components/answer.test.jsx b/frontend/tests/components/answer.test.jsx new file mode 100644 index 00000000..ad1a6cc7 --- /dev/null +++ b/frontend/tests/components/answer.test.jsx @@ -0,0 +1,252 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import Answer from "../../src/components/Answer/Answer"; +import "@testing-library/jest-dom"; +import { afterEach, vi } from "vitest"; + +describe("Answer", () => { + afterEach(() => { + localStorage.clear(); + }) + + test("All answers shown on page", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + shuffledArtistAnswerList.forEach((artist) => { + expect(screen.getByText(artist)).toBeInTheDocument(); + }); + }); + + test('Button changes to green when correct answer is clicked', () => { + const selectedTrack = { + title: "Correct Track Title", + artist: "Correct Artist", + album: { title: "Correct Album Title" } + }; + // const selectedTrack = {artist: "correct-answer"}; + const shuffledArtistAnswerList = ['Artist 1' , 'Artist 2', 'Artist 3', 'Correct Artist']; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText('Correct Artist')) + expect(screen.getByText('Correct Artist')).toHaveClass('bg-correct-color') + }) + + test('Button changes to red when incorrect answer is clicked', () => { + const selectedTrack = { + title: "Correct Track Title", + artist: "Correct Artist", + album: { title: "Correct Album Title" } + }; + // const selectedTrack = {artist: "correct-answer"}; + const shuffledArtistAnswerList = ['Artist 1' , 'Artist 2', 'Artist 3', 'Correct Artist']; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText('Artist 1')) + expect(screen.getByText('Artist 1')).toHaveClass('bg-incorrect-color') + fireEvent.click(screen.getByText('Artist 2')) + expect(screen.getByText('Artist 2')).toHaveClass('bg-incorrect-color') + fireEvent.click(screen.getByText('Artist 3')) + expect(screen.getByText('Artist 3')).toHaveClass('bg-incorrect-color') + }) + + test("Score is 0 at the start", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + expect(screen.getByText("Your Score: 0")).toBeInTheDocument(); + }); + + test("Score is updated to 100 when correct answer is clicked, bonus of 50 points if less than 5 seconds", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText("correct-answer")); + expect(screen.getByText("Your Score: 100")).toBeInTheDocument(); + expect(screen.getByText("Speed Bonus: 50")).toBeInTheDocument(); + }); + + test("Score is updated to 100 when correct answer is clicked, no bonus if more than 5 seconds", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText("correct-answer")); + expect(screen.getByText("Your Score: 100")).toBeInTheDocument(); + expect(screen.getByText("Speed Bonus: 0")).toBeInTheDocument(); + }); + + test("Score stays at 0 when incorrect answer is clicked and less than 5 seconds", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText("Artist 1")); + expect(screen.getByText("Your Score: 0")).toBeInTheDocument(); + expect(screen.getByText("Speed Bonus: 0")).toBeInTheDocument(); + }); + + + test("Score stays at 0 when incorrect answer is clicked and more than 5 seconds", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText("Artist 1")); + expect(screen.getByText("Your Score: 0")).toBeInTheDocument(); + expect(screen.getByText("Speed Bonus: 0")).toBeInTheDocument(); + }); + + test("Buttons are disabled when interactionDisabled is true", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText("Artist 1")); + expect(screen.getByText("Artist 1")).toBeDisabled(); + expect(screen.getByText("Artist 2")).toBeDisabled(); + expect(screen.getByText("Artist 3")).toBeDisabled(); + expect(screen.getByText("correct-answer")).toBeDisabled(); + }); + + test("Score does not change when interactionDisabled is true", () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText("correct-answer")); + expect(screen.getByText("Your Score: 0")).toBeInTheDocument(); + }); + + test("Button colors reset after a certain time", async () => { + const selectedTrack = { artist: "correct-answer" }; + const shuffledArtistAnswerList = [ + "Artist 1", + "Artist 2", + "Artist 3", + "correct-answer", + ]; + const mockOnAnswerButtonClick = vi.fn(); + render( + + ); + fireEvent.click(screen.getByText("correct-answer")); + expect(screen.getByText("correct-answer")).toHaveClass("bg-correct-color"); + await new Promise((resolve) => setTimeout(resolve, 1600)); + expect(screen.getByText("correct-answer")).toHaveClass("bg-box-color"); + }); +}); diff --git a/frontend/tests/components/audioButton.test.jsx b/frontend/tests/components/audioButton.test.jsx new file mode 100644 index 00000000..b5af5736 --- /dev/null +++ b/frontend/tests/components/audioButton.test.jsx @@ -0,0 +1,59 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { vi } from "vitest"; +import AudioButton from "../../src/components/AudioButton/AudioButton"; +import "@testing-library/jest-dom"; + +describe("AudioButton", () => { + test("AudioButton before clicked", () => { + const mockOnPlayPause = vi.fn(); + render( + + ); + const playButton = screen.getByRole("button"); + expect(playButton.textContent).toBe("▶"); + }); + + test("AudioButton changes state to pause when play button state is true", () => { + const playStub = vi + .spyOn(window.HTMLMediaElement.prototype, "play") + .mockImplementation(() => {}); + const mockOnPlayPause = vi.fn(); + render( + + ); + const playButton = screen.getByRole("button"); + fireEvent.click(playButton); + expect(playStub).toHaveBeenCalled(); + expect(mockOnPlayPause).toHaveBeenCalled(); + expect(playButton.textContent).toBe("❚❚"); + playStub.mockRestore(); + }); + + test("AudioButton changes state to play when clicked", () => { + const playStub = vi + .spyOn(window.HTMLMediaElement.prototype, "play") + .mockImplementation(() => {}); + const mockOnPlayPause = vi.fn(); + render( + + ); + const playButton = screen.getByRole("button"); + fireEvent.click(playButton); + expect(playStub).toHaveBeenCalled(); + expect(mockOnPlayPause).toHaveBeenCalled(); + expect(playButton.textContent).toBe("▶"); + playStub.mockRestore(); + }); +}); diff --git a/frontend/tests/components/genrePicker.test.jsx b/frontend/tests/components/genrePicker.test.jsx new file mode 100644 index 00000000..e5cdab48 --- /dev/null +++ b/frontend/tests/components/genrePicker.test.jsx @@ -0,0 +1,27 @@ +import GenrePicker from "../../src/components/GenrePicker/GenrePicker"; +import { vi } from 'vitest'; +import { render, fireEvent, screen } from "@testing-library/react"; + +describe('GenrePicker Component', () => { + const mockOnGenreSelect = vi.fn(); + + test('calls onGenreSelect with correct genre ID when a genre button is clicked', () => { + const genreId = 132; + render(); + const playButton = screen.getByText('Pop'); + fireEvent.click(playButton); + expect(mockOnGenreSelect).toHaveBeenCalledWith(genreId, "bg-pop"); + }); + + test('all the buttons for all genres are created', () => { + render(); + expect(screen.getByText('Pop')).toBeTruthy(); + expect(screen.getByText('Rap/Hiphop')).toBeTruthy(); + expect(screen.getByText('RnB')).toBeTruthy(); + expect(screen.getByText('Dance')).toBeTruthy(); + expect(screen.getByText('Films/Games')).toBeTruthy(); + expect(screen.getByText('Metal')).toBeTruthy(); + }) +}); + + diff --git a/frontend/tests/components/question.test.jsx b/frontend/tests/components/question.test.jsx new file mode 100644 index 00000000..c1b63cb2 --- /dev/null +++ b/frontend/tests/components/question.test.jsx @@ -0,0 +1,26 @@ +import { screen, render } from "@testing-library/react"; +import Question from "../../src/components/Question/Question"; +import "@testing-library/jest-dom"; + +describe("Question component", () => { + test("Question is generated for track title", () => { + render(); + expect(screen.queryByText("What is the name of the track?")).toBeInTheDocument(); + }); + + test("Question is generated for artist name", () => { + render(); + expect(screen.queryByText("What is the name of the artist?")).toBeInTheDocument(); + }); + + test("Question is generated for album title", () => { + render(); + expect(screen.queryByText("What is the name of the album?")).toBeInTheDocument(); + }); + + test("Question is hidden from view", () => { + render(