diff --git a/.eslintrc.js b/.eslintrc.js index 3a42e2c5..6d71f310 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,7 @@ module.exports = { }, plugins: ["@typescript-eslint", "react"], rules: { - indent: ["error", "tab"], + indent: ["warn", "tab"], quotes: ["error", "double", { allowTemplateLiterals: true }], semi: ["error", "always"], }, diff --git a/.gitignore b/.gitignore index 38669c29..ed7fd988 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .pnp.js .yarn/install-state.gz + # testing /coverage diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index 3dd65ef1..86e3cd0e 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -20,6 +20,22 @@ const schema = a.schema({ allow.guest().to(["read"]), ]; }), + + Leaderboard: a + .model({ + id: a.id().required(), + username: a.string().required(), + score: a.integer().required().default(0), + year: a.integer().required().default(2024), + }) + .secondaryIndexes((index) => [index("year").sortKeys(["score"]).queryField("listByScore")]) + .authorization((allow) => { + return [ + allow.group("directors").to(["create", "delete", "read"]), + allow.authenticated("identityPool").to(["read", "create"]), + allow.guest().to(["read", "create"]), + ]; + }), }); export type Schema = ClientSchema; diff --git a/app/2048/leaderboard/page.tsx b/app/2048/leaderboard/page.tsx new file mode 100644 index 00000000..abd09929 --- /dev/null +++ b/app/2048/leaderboard/page.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useState } from "react"; +import { useEffect } from "react"; +import NavBar from "@/components/nav-bar/nav-bar"; +import Footer from "@/components/footer/footer"; +import "@/app/globals.css"; +import HackRPIButton from "@/components/themed-components/hackrpi-button"; +import { get_leaderboard, LeaderboardEntry } from "@/app/actions"; +import * as Auth from "@aws-amplify/auth"; +import { Amplify } from "aws-amplify"; +import amplify_outputs from "@/amplify_outputs.json"; +import { generateClient } from "aws-amplify/api"; +import { Schema } from "@/amplify/data/resource"; + +Amplify.configure(amplify_outputs); +const client = generateClient({ authMode: "userPool" }); + +export default function Page() { + const [leaderboardEntries, setLeaderboardEntries] = useState([]); + const [isDirector, setIsDirector] = useState(false); + + const fetchLeaderboard = async () => { + const entries = await get_leaderboard(); + setLeaderboardEntries(entries); + }; + + async function is_director() { + let groups = undefined; + try { + const session = await Auth.fetchAuthSession(); + groups = session.tokens?.accessToken.payload["cognito:groups"]; + } catch (e) { + console.error(e); + groups = undefined; + } + + return groups !== undefined; + } + + useEffect(() => { + const setDirectorStatus = async () => { + setIsDirector(await is_director()); + }; + + setDirectorStatus(); + fetchLeaderboard(); + }, []); + + return ( +
+ + +
+

2048 Leaderboard

+ + + + + + + {isDirector ? : null} + + + + + {leaderboardEntries.map((entry, index) => ( + + + + + {isDirector ? ( + + ) : null} + + ))} + +
PositionUsernameScoreDelete
{index + 1}{entry.username}{entry.score} + { + await deleteLeaderboardEntry(entry.id); + await fetchLeaderboard(); + }} + > + Delete Item + +
+
+
+ +
+
+
+
+ ); +} + +async function deleteLeaderboardEntry(id: string) { + if (!confirm("Are you sure???")) { + return; + } + + const response = await client.models.Leaderboard.delete({ id }); + + if (response.errors) { + alert("Error deleting leaderboard entry"); + } +} diff --git a/app/2048/page.tsx b/app/2048/page.tsx new file mode 100644 index 00000000..6a5f8a03 --- /dev/null +++ b/app/2048/page.tsx @@ -0,0 +1,436 @@ +"use client"; + +import { useState } from "react"; +import { useEffect } from "react"; +import NavBar from "@/components/nav-bar/nav-bar"; +import Board from "@/components/game/board"; +import GameOver from "@/components/game/game-over"; +import HackRPIButton from "@/components/themed-components/hackrpi-button"; +import * as Auth from "@aws-amplify/auth"; + +import { create_leaderboard_entry, is_game_ready } from "@/app/actions"; + +import "@/app/globals.css"; + +export default function Page() { + const [grid, setGrid] = useState([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]); + + const [score, setScore] = useState(0); + const [gameOver, setIsGameOver] = useState(false); + let startX = 0; + let startY = 0; + let endX = 0; + let endY = 0; + + const initializeGame = () => { + let newGrid = [...grid]; + newGrid = placeRandomTile(newGrid); + newGrid = placeRandomTile(newGrid); + setGrid(newGrid); + }; + + const placeRandomTile = (grid: number[][]): number[][] => { + const emptyTiles: Array<[number, number]> = []; + + grid.forEach((row, rowIndex) => { + row.forEach((tile, colIndex) => { + if (tile === 0) { + emptyTiles.push([rowIndex, colIndex]); + } + }); + }); + + if (emptyTiles.length > 0) { + const randomIndex = Math.floor(Math.random() * emptyTiles.length); + const [row, col] = emptyTiles[randomIndex]; + grid[row][col] = Math.random() > 0.5 ? 2 : 4; + } + + return grid; + }; + + const addRandomTile = (grid: number[][]): number[][] => { + const emptyPositions: [number, number][] = []; + + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[i].length; j++) { + if (grid[i][j] === 0) { + emptyPositions.push([i, j]); + } + } + } + + if (emptyPositions.length === 0) { + return grid; + } + + const [row, col] = emptyPositions[Math.floor(Math.random() * emptyPositions.length)]; + + const newValue = Math.random() < 0.9 ? 2 : 4; + + grid[row][col] = newValue; + + return grid; + }; + + const handleSwipe = ( + startx: number, + starty: number, + endx: number, + endy: number, + setGrid: (gridUpdater: (prevGrid: number[][]) => number[][]) => void, + ) => { + const deltax = endx - startx; + const deltay = endy - starty; + + setGrid((prevGrid: number[][]) => { + let newGrid = [...prevGrid]; + let moved = false; + + if (deltax > 0 && Math.abs(deltax) > Math.abs(deltay)) { + const rightGrid = moveRight(newGrid); + moved = JSON.stringify(rightGrid) !== JSON.stringify(newGrid); + newGrid = rightGrid; + } else if (deltax < 0 && Math.abs(deltax) > Math.abs(deltay)) { + const leftGrid = moveLeft(newGrid); + moved = JSON.stringify(leftGrid) !== JSON.stringify(newGrid); + newGrid = leftGrid; + } else if (deltay < 0 && Math.abs(deltax) < Math.abs(deltay)) { + const upGrid = moveUp(newGrid); + moved = JSON.stringify(upGrid) !== JSON.stringify(newGrid); + newGrid = upGrid; + } else if (deltay > 0 && Math.abs(deltax) < Math.abs(deltay)) { + const downGrid = moveDown(newGrid); + moved = JSON.stringify(downGrid) !== JSON.stringify(newGrid); + newGrid = downGrid; + } + + if (moved) { + newGrid = addRandomTile(newGrid); + } + + setIsGameOver(isGameOver(newGrid)); + + return newGrid; + }); + }; + + // Handle touch start + const handleTouchStart = (event: React.TouchEvent) => { + startX = event.touches[0].clientX; + startY = event.touches[0].clientY; + }; + + // Handle touch move + const handleTouchMove = (event: React.TouchEvent) => { + endX = event.touches[0].clientX; + endY = event.touches[0].clientY; + }; + + const handleTouchEnd = () => { + handleSwipe(startX, startY, endX, endY, setGrid); + }; + + const handleKeyPress = ( + e: KeyboardEvent, + grid: number[][], + setGrid: (gridUpdater: (prevGrid: number[][]) => number[][]) => void, + ) => { + setGrid((prevGrid: number[][]) => { + let newGrid = [...prevGrid]; + let moved = false; + + if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { + e.preventDefault(); + } + + switch (e.key) { + case "ArrowUp": + // eslint-disable-next-line + const upGrid = moveUp(newGrid); + moved = JSON.stringify(upGrid) !== JSON.stringify(newGrid); + newGrid = upGrid; + break; + case "ArrowDown": + // eslint-disable-next-line + const downGrid = moveDown(newGrid); + moved = JSON.stringify(downGrid) !== JSON.stringify(newGrid); + newGrid = downGrid; + break; + case "ArrowLeft": + // eslint-disable-next-line + const leftGrid = moveLeft(newGrid); + moved = JSON.stringify(leftGrid) !== JSON.stringify(newGrid); + newGrid = leftGrid; + break; + case "ArrowRight": + // eslint-disable-next-line + const rightGrid = moveRight(newGrid); + moved = JSON.stringify(rightGrid) !== JSON.stringify(newGrid); + newGrid = rightGrid; + break; + default: + return prevGrid; + } + + if (moved) { + newGrid = addRandomTile(newGrid); + } + + setIsGameOver(isGameOver(newGrid)); + + return newGrid; + }); + }; + + const moveLeft = (grid: number[][]): number[][] => { + let newScore = 0; + const newGrid = grid.map((row) => { + const filteredRow = row.filter((tile) => tile !== 0); + + for (let i = 0; i < filteredRow.length - 1; i++) { + if (filteredRow[i] === filteredRow[i + 1]) { + filteredRow[i] *= 2; + newScore += filteredRow[i] / 2; + filteredRow[i + 1] = 0; + i++; + } + } + + const mergedRow = filteredRow.filter((tile) => tile !== 0); + + while (mergedRow.length < row.length) { + mergedRow.push(0); + } + + return mergedRow; + }); + + setScore((prevScore) => prevScore + newScore); + return newGrid; + }; + + const moveRight = (grid: number[][]): number[][] => { + return grid.map((row) => { + const reversedRow = row.slice().reverse(); + const newRow = moveLeft([reversedRow])[0]; + return newRow.reverse(); + }); + }; + + const moveUp = (grid: number[][]): number[][] => { + const newGrid: number[][] = Array.from({ length: grid.length }, () => new Array(grid[0].length).fill(0)); + let newScore = 0; + + for (let col = 0; col < grid[0].length; col++) { + const filteredColumn = grid.map((row) => row[col]).filter((tile) => tile !== 0); + + for (let i = 0; i < filteredColumn.length - 1; i++) { + if (filteredColumn[i] === filteredColumn[i + 1]) { + filteredColumn[i] *= 2; + newScore += filteredColumn[i] / 2; + filteredColumn[i + 1] = 0; + i++; + } + } + + const mergedColumn = filteredColumn.filter((tile) => tile !== 0); + + while (mergedColumn.length < grid.length) { + mergedColumn.push(0); + } + + for (let row = 0; row < grid.length; row++) { + if (!newGrid[row]) { + newGrid[row] = []; + } + newGrid[row][col] = mergedColumn[row]; + } + } + + setScore((prevScore) => prevScore + newScore); + return newGrid; + }; + + const moveDown = (grid: number[][]): number[][] => { + const newGrid: number[][] = Array.from({ length: grid.length }, () => new Array(grid[0].length).fill(0)); + let newScore = 0; + + for (let col = 0; col < grid[0].length; col++) { + const filteredColumn = grid.map((row) => row[col]).filter((tile) => tile !== 0); + + for (let i = filteredColumn.length - 1; i > 0; i--) { + if (filteredColumn[i] === filteredColumn[i - 1]) { + filteredColumn[i] *= 2; + newScore += filteredColumn[i] / 2; + filteredColumn[i - 1] = 0; + i++; + } + } + + const mergedColumn = filteredColumn.filter((tile) => tile !== 0); + + while (mergedColumn.length < grid.length) { + mergedColumn.unshift(0); + } + + for (let row = 0; row < grid.length; row++) { + newGrid[row][col] = mergedColumn[row]; + } + } + + setScore((prevScore) => prevScore + newScore); + return newGrid; + }; + + const resetGame = () => { + const newGrid = Array(4) + .fill(null) + .map(() => Array(4).fill(0)); + + for (let i = 0; i < 2; i++) { + addRandomTile(newGrid); + } + + setScore(0); + return newGrid; + }; + + const handleReset = () => { + setGrid(resetGame()); + }; + + const isGameOver = (grid: number[][]): boolean => { + for (const row of grid) { + if (row.includes(0)) return false; + } + + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[i].length; j++) { + if ( + (j < grid[i].length - 1 && grid[i][j] === grid[i][j + 1]) || + (i < grid.length - 1 && grid[i][j] === grid[i + 1][j]) + ) { + return false; + } + } + } + + return true; + }; + + const is_director = async () => { + const session = await Auth.fetchAuthSession(); + const groups = session.tokens?.accessToken.payload["cognito:groups"]; + return groups !== undefined; + }; + + const handleCloseGameOver = () => { + setIsGameOver(false); + setGrid(resetGame()); + }; + + const handleSubmit = async (username: string) => { + const response = await create_leaderboard_entry({ + username, + score, + boardState: grid, + }); + + if (response.status === 200) { + handleCloseGameOver(); + } else { + alert(response.message); + } + }; + + const handleExit = () => { + setIsGameOver(false); + }; + const [gameReady, setGameReady] = useState(false); + + useEffect(() => { + const checkGameReady = async () => { + const response = await is_game_ready(); + setGameReady(response); + }; + + const handleKeyDown = (e: KeyboardEvent) => handleKeyPress(e, grid, setGrid); + + window.addEventListener("keydown", handleKeyDown); + + window.addEventListener("load", () => { + document.body.style.paddingTop = "1px"; + }); + + checkGameReady(); + initializeGame(); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + if (gameReady === false) { + return ( +
+ + +
+
+

+ 2048 Will be available to play at 10:00 AM November 9th to 11:00 AM November 11th. +

+
+

+ 2048 +

+
+
+
+
+
+ ); + } + + return ( +
+ + +
+
+
+

Use arrow keys or swipe to move blocks.

+
+ + Reset Game + +

+ 2048 +

+

Score: {score}

+
+
+ +
+ {gameOver && } +
+
+
+
+ ); +} diff --git a/app/actions.ts b/app/actions.ts new file mode 100644 index 00000000..f0d545fe --- /dev/null +++ b/app/actions.ts @@ -0,0 +1,160 @@ +"use server"; + +import { Amplify } from "aws-amplify"; +import * as Auth from "@aws-amplify/auth"; +import { generateClient } from "aws-amplify/api"; +import type { Schema } from "@/amplify/data/resource"; +// eslint-disable-next-line +// @ts-ignore +import amplify_outputs from "@/amplify_outputs.json"; + +import { Profanity } from "@2toad/profanity"; +import { Event } from "@/data/schedule"; + +Amplify.configure(amplify_outputs); +const client = generateClient({ authMode: "identityPool" }); + +const isAlphanumeric = (username: string) => { + return /^[a-z0-9]+$/i.test(username); +}; + +export type LeaderboardEntry = { + id: string; + username: string; + score: number; + year: number; +}; + +export async function is_game_ready() { + const now = new Date(); + const start = new Date(1731160800000); // November 9, 2024 10:00:00 AM + const end = new Date(1731254400000); // November 10, 2024 11:00:00 AM + + return now >= start && now <= end; +} + +export async function get_leaderboard() { + let groups = undefined; + try { + const session = await Auth.fetchAuthSession(); + groups = session.tokens?.accessToken.payload["cognito:groups"]; + } catch (e) { + console.error(e); + groups = undefined; + } + + const { data, errors } = await client.models.Leaderboard.listByScore( + { + year: 2024, + }, + { + limit: 50, + sortDirection: "DESC", + authMode: groups ? "userPool" : "identityPool", + }, + ); + + if (errors) { + console.error(errors); + return []; + } + + return data.map((entry) => entry as LeaderboardEntry); +} + +export async function create_leaderboard_entry({ + username, + score, + boardState, +}: { + username: string; + score: number; + boardState: number[][]; +}) { + const profanity = new Profanity({ + wholeWord: false, + languages: ["en", "de", "es", "fr"], + }); + + profanity.addWords(["fvck", "shjt", "bjtch", "njgga", "njgger", "f4ck"]); + + if (profanity.exists(username) || username.length > 20 || !isAlphanumeric(username)) { + return { status: 401, message: "Usernames must be alphanumeric and less than 20 characters." }; + } + + const scoreUpperBounds: { [key: number]: number } = { + 64: 2000, + 128: 4000, + 256: 10000, + 512: 30000, + 1024: 60000, + 2048: 100000, + 4096: 150000, + 8192: 200000, + }; + + const maxTile = Math.max(...boardState.map((row) => Math.max(...row))); + + if ( + score < 0 || + score > 200000 || + isNaN(score) || + score % 4 !== 0 || + (score % 10 === 0 && score % 20 !== 0) || + score > scoreUpperBounds[maxTile] + ) { + return { status: 401, message: "Invalid score." }; + } + + const { errors } = await client.models.Leaderboard.create( + { + username, + score, + year: new Date().getFullYear(), + }, + { + authMode: "identityPool", + }, + ); + + if (errors) { + console.error(errors); + return { status: 500, message: "Failed to create leaderboard entry. Please Try Again." }; + } + + return { status: 200, message: "Success" }; +} + +export async function fetchEvents(): Promise<{ + status: number; + message: string; + events: Event[]; +}> { + let groups = undefined; + try { + const session = await Auth.fetchAuthSession(); + groups = session.tokens?.accessToken.payload["cognito:groups"]; + } catch (e) { + console.error(e); + groups = undefined; + } + + const { data, errors } = await client.models.event.list({ + authMode: groups ? "userPool" : "identityPool", + limit: 200, + filter: { + visible: { eq: true }, + }, + }); + + if (errors) { + console.error(errors); + return { status: 500, message: "Failed to fetch events.", events: [] }; + } + + return { + status: 200, + message: "Success", + events: data.map((event) => event as Event), + }; +} diff --git a/app/event/schedule/page.tsx b/app/event/schedule/page.tsx index 03389540..d46329c6 100644 --- a/app/event/schedule/page.tsx +++ b/app/event/schedule/page.tsx @@ -6,12 +6,7 @@ import Footer from "@/components/footer/footer"; import { useEffect, useState } from "react"; import type { Event } from "@/data/schedule"; import { SATURDAY_END, SATURDAY_START, SUNDAY_END, SUNDAY_START, saturdayTimes, sundayTimes } from "@/data/schedule"; - -import { Amplify } from "aws-amplify"; -// eslint-disable-next-line -// @ts-ignore -import awsconfig from "@/amplify_outputs.json"; -import * as Auth from "aws-amplify/auth"; +import { fetchEvents } from "@/app/actions"; import { generateClient } from "aws-amplify/data"; import type { Schema } from "@/amplify/data/resource"; @@ -19,9 +14,6 @@ import HappeningNow from "@/components/schedule/happening-now"; import Schedule from "@/components/schedule/schedule"; import HackRPILink from "@/components/themed-components/hackrpi-link"; -Amplify.configure(awsconfig); -const client = generateClient({}); - export default function Page() { const [currentDateTime, setCurrentDateTime] = useState(new Date()); const [allEvents, setAllEvents] = useState([]); @@ -31,37 +23,14 @@ export default function Page() { const [happeningNow, setHappeningNow] = useState([]); const [modalEvent, setModalEvent] = useState(null); - async function fetchEvents(): Promise { - let groups = undefined; - try { - const session = await Auth.fetchAuthSession(); - groups = session.tokens?.accessToken.payload["cognito:groups"]; - } catch (e) { - console.error(e); - groups = undefined; - } - - const { data, errors } = await client.models.event.list({ - authMode: groups ? "userPool" : "identityPool", - limit: 200, - filter: { - visible: { eq: true }, - }, - }); - - if (errors) { - setState("error"); - console.error(errors); - return []; - } - - setState("loaded"); - return data.map((event) => event as Event); - } - useEffect(() => { - fetchEvents().then((events) => { - const saturdayEvents = events + fetchEvents().then((resp) => { + if (resp.status !== 200) { + setState("error"); + return; + } + + const saturdayEvents = resp.events .slice() .map((event) => { if (event.startTime >= SATURDAY_START && event.startTime < SATURDAY_END) { @@ -77,7 +46,7 @@ export default function Page() { .filter((event) => event !== null && event.endTime > event.startTime) .sort((a, b) => a!.startTime - b!.startTime) as Event[]; - const sundayEvents = events + const sundayEvents = resp.events .slice() .map((event) => { if ( @@ -101,9 +70,10 @@ export default function Page() { setSaturdayEvents(saturdayEvents); setSundayEvents(sundayEvents); - setAllEvents(events); + setAllEvents(resp.events); - setHappeningNow(determineHappeningNow(events)); + setHappeningNow(determineHappeningNow(resp.events)); + setState("loaded"); }); const interval = setInterval(() => { diff --git a/components/game/board.tsx b/components/game/board.tsx new file mode 100644 index 00000000..aac328c9 --- /dev/null +++ b/components/game/board.tsx @@ -0,0 +1,17 @@ +import Tile from "./tile"; + +function Board({ grid }: { grid: number[][] }) { + return ( +
+ {grid.map((row, rowIndex) => + row.map((tile, colIndex) => ( +
+ +
+ )), + )} +
+ ); +} + +export default Board; diff --git a/components/game/game-over.tsx b/components/game/game-over.tsx new file mode 100644 index 00000000..b10e530b --- /dev/null +++ b/components/game/game-over.tsx @@ -0,0 +1,33 @@ +import HackRPIButton from "../themed-components/hackrpi-button"; + +const GameOver = ({ + onSubmitClose, + onExitClose, +}: { + onSubmitClose: (username: string) => void; + onExitClose: () => void; +}) => { + const onSubmit = () => { + const username = (document.getElementById("username")! as HTMLInputElement).value; + onSubmitClose(username); + }; + + return ( +
+
event.stopPropagation()} + > +

Game Over!

+

+ Abusing the game leaderboard may result in your score being removed or you being banned from playing entirely. +

+ + + Submit and Try Again +
+
+ ); +}; + +export default GameOver; diff --git a/components/game/tile.tsx b/components/game/tile.tsx new file mode 100644 index 00000000..91529006 --- /dev/null +++ b/components/game/tile.tsx @@ -0,0 +1,69 @@ +function Tile({ value }: { value: number }) { + let bgColor; + let textColor; + + switch (value) { + case 0: + bgColor = "bg-gray-200"; + break; + case 2: + bgColor = "bg-radial-yellow-200"; + textColor = "text-gray-700"; + break; + case 4: + bgColor = "bg-radial-yellow-300"; + textColor = "text-gray-600"; + break; + case 8: + bgColor = "bg-radial-yellow-400"; + textColor = "text-gray-500"; + break; + case 16: + bgColor = "bg-radial-yellow-500"; + textColor = "text-gray-400"; + break; + case 32: + bgColor = "bg-radial-yellow-600"; + textColor = "text-gray-300"; + break; + case 64: + bgColor = "bg-radial-yellow-700"; + textColor = "text-gray-200"; + break; + case 128: + bgColor = "bg-radial-green-200"; + textColor = "text-gray-700"; + break; + case 256: + bgColor = "bg-radial-green-300"; + textColor = "text-gray-600"; + break; + case 512: + bgColor = "bg-radial-green-400"; + textColor = "text-gray-500"; + break; + case 1024: + bgColor = "bg-radial-green-500"; + textColor = "text-gray-400"; + break; + case 2048: + bgColor = "bg-radial-green-600"; + textColor = "text-gray-300"; + break; + default: + bgColor = "bg-black"; + textColor = "text-gray-100"; + } + + return ( +
+ {value !== 0 ? value : ""} +
+ ); +} + +export default Tile; diff --git a/components/nav-bar/nav-bar.tsx b/components/nav-bar/nav-bar.tsx index 9f738c47..9e96c557 100644 --- a/components/nav-bar/nav-bar.tsx +++ b/components/nav-bar/nav-bar.tsx @@ -41,6 +41,13 @@ export const links: NavGroup[] = [ { href: "/resources#submissions", children: "Submitting Your Project" }, ], }, + { + name: "2048", + links: [ + { href: "/2048", children: "Play" }, + { href: "/2048/leaderboard", children: "Leaderboard" }, + ], + }, ]; export default function NavBar({ showOnScroll }: { showOnScroll: boolean }) { diff --git a/package-lock.json b/package-lock.json index b2865fd1..a7851019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,12 @@ "name": "hackrpi-website-2024", "version": "0.1.0", "dependencies": { + "@2toad/profanity": "^3.0.1", "@aws-amplify/ui-react": "^6.5.5", "aws-amplify": "^6.6.6", "next": "^14.2.16", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-google-slides": "^4.0.0", "react-multi-carousel": "^2.8.5", "sharp": "^0.33.5" }, @@ -43,6 +43,14 @@ "typescript": "^5.6.3" } }, + "node_modules/@2toad/profanity": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@2toad/profanity/-/profanity-3.0.1.tgz", + "integrity": "sha512-WqAljnXnxsdUmwJT2VDTHZ6IAMgktnobLtBSdGFQYFLB1kh7q+QqzpxY4rRAWU4lSYvGUykJ+xv5wJ3IZYBuzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -28947,20 +28955,6 @@ "react": "^18.3.1" } }, - "node_modules/react-google-slides": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/react-google-slides/-/react-google-slides-4.0.0.tgz", - "integrity": "sha512-WRstUSvMaGmeC7GWobZsEj5tRw1h5/vlf1hs4LGNuuKrdzG5B52HV3eU3Okk+MDo309dVDnjYaAxW/p5kbLPDQ==", - "license": "MIT", - "engines": { - "node": ">=8", - "npm": ">=5" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-hook-form": { "version": "7.53.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", diff --git a/package.json b/package.json index fe335f3c..a8a025b4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@2toad/profanity": "^3.0.1", "@aws-amplify/ui-react": "^6.5.5", "aws-amplify": "^6.6.6", "next": "^14.2.16", diff --git a/tailwind.config.ts b/tailwind.config.ts index 3f40b2cf..5f9e319c 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -36,6 +36,22 @@ const config: Config = { xs: "475px", "2xs": "375px", }, + backgroundImage: { + "radial-yellow-200": "radial-gradient(circle, #fef08a, #fde047)", + "radial-yellow-300": "radial-gradient(circle, #fde047, #facc15)", + "radial-yellow-400": "radial-gradient(circle, #facc15, #eab308)", + "radial-yellow-500": "radial-gradient(circle, #eab308, #ca8a04)", + "radial-yellow-600": "radial-gradient(circle, #ca8a04, #a16207)", + "radial-yellow-700": "radial-gradient(circle, #a16207, #854d0e)", + "radial-yellow-800": "radial-gradient(circle, #854d0e, #713f12)", + "radial-green-200": "radial-gradient(circle, #a7f3d0, #6ee7b7)", + "radial-green-300": "radial-gradient(circle, #6ee7b7, #34d399)", + "radial-green-400": "radial-gradient(circle, #34d399, #10b981)", + "radial-green-500": "radial-gradient(circle, #10b981, #059669)", + "radial-green-600": "radial-gradient(circle, #059669, #047857)", + "radial-green-700": "radial-gradient(circle, #047857, #065f46)", + "radial-green-800": "radial-gradient(circle, #065f46, #064e3b)", + }, }, }, daisyui: {