From c95cadffa0a27b600867e568ae5ca2b8d50da22c Mon Sep 17 00:00:00 2001 From: Sistav Date: Sat, 23 May 2026 16:50:01 -0400 Subject: [PATCH] Update requirements and minor features --- client/src/pages/Leaderboard.js | 122 ++++++++++++++++++++++++-------- client/src/pages/Type.js | 2 +- server/package.json | 9 +-- server/server.js | 55 +++++++------- 4 files changed, 125 insertions(+), 63 deletions(-) diff --git a/client/src/pages/Leaderboard.js b/client/src/pages/Leaderboard.js index b83c8c4..b8955aa 100644 --- a/client/src/pages/Leaderboard.js +++ b/client/src/pages/Leaderboard.js @@ -5,14 +5,15 @@ import "../App.css"; function Leaderboard() { const [submissions, setSubmissions] = useState([]); const [message, setMessage] = useState(""); + const [sort, setSort] = useState("top"); useEffect(() => { - loadLeaderboard(); - }, []); + loadLeaderboard(sort); + }, [sort]); - const loadLeaderboard = async () => { + const loadLeaderboard = async (sortMode) => { try { - const response = await fetch("/api/leaderboard"); + const response = await fetch(`/api/leaderboard?sort=${sortMode}`); const data = await response.json(); setSubmissions(data); } catch (error) { @@ -20,7 +21,6 @@ function Leaderboard() { } }; - const upvote = async (id) => { try { const response = await fetch("/api/upvote", { @@ -33,7 +33,7 @@ function Leaderboard() { if (response.ok) { setMessage("✓ Vote recorded!"); - loadLeaderboard(); + loadLeaderboard(sort); setTimeout(() => setMessage(""), 3000); } else { setMessage("✗ " + (data.error || "Failed to vote")); @@ -52,6 +52,10 @@ function Leaderboard() { }); }; + const toggleSort = () => { + setSort((prev) => (prev === "top" ? "new" : "top")); + }; + return (

Leaderboard

- +
+ + + {/* Sliding toggle */} +
+ {/* Sliding highlight */} +
+ + Top + + + New + +
+
@@ -111,7 +171,7 @@ function Leaderboard() {
{submissions.length === 0 ? ( -

+

No submissions yet. Be the first!

) : ( @@ -123,7 +183,7 @@ function Leaderboard() { marginBottom: "15px", borderRadius: "8px", backgroundColor: "#1a1a1a", - border: `1px solid ${index < 3 ? "#555" : "#333"}`, + border: `1px solid ${index < 3 && sort === "top" ? "#555" : "#333"}`, display: "flex", alignItems: "center", gap: "20px", @@ -134,12 +194,12 @@ function Leaderboard() { fontSize: "24px", minWidth: "40px", color: - index === 0 - ? "#fff" - : index === 1 - ? "#aaa" - : index === 2 - ? "#888" + sort === "top" && index === 0 + ? "#FFD700" + : sort === "top" && index === 1 + ? "#C0C0C0" + : sort === "top" && index === 2 + ? "#CD7F32" : "#555", }} > @@ -157,7 +217,7 @@ function Leaderboard() { > "{sub.phrase}"
-
+
{formatDate(sub.created_at)}
diff --git a/client/src/pages/Type.js b/client/src/pages/Type.js index 8558f02..4d894c4 100644 --- a/client/src/pages/Type.js +++ b/client/src/pages/Type.js @@ -166,7 +166,7 @@ function Type() { const response = await fetch("/api/submit", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ phrase, timestamps }), + body: JSON.stringify({ timestamps }), }); const data = await response.json(); diff --git a/server/package.json b/server/package.json index df776b2..75b52c8 100644 --- a/server/package.json +++ b/server/package.json @@ -7,13 +7,14 @@ "dev": "nodemon server.js" }, "dependencies": { - "express": "^4.18.2", - "ws": "^8.16.0", + "cors": "^2.8.5", "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^8.2.0", "sql.js": "^1.10.3", - "cors": "^2.8.5" + "ws": "^8.16.0" }, "devDependencies": { "nodemon": "^3.0.2" } -} \ No newline at end of file +} diff --git a/server/server.js b/server/server.js index 173443c..5b977f0 100644 --- a/server/server.js +++ b/server/server.js @@ -9,6 +9,7 @@ import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); import cors from 'cors'; +import helmet from 'helmet'; const app = express(); const server = createServer(app); @@ -98,6 +99,7 @@ function saveDB() { } // Middleware +app.use(helmet()); app.use(cors()); app.use(express.json()); app.set('trust proxy', 1); @@ -178,13 +180,18 @@ wss.on('connection', (ws) => { // Submit phrase app.post('/api/submit', (req, res) => { - const { phrase, timestamps } = req.body; + const { timestamps } = req.body; const ip = getClientIP(req); - if (!phrase || !timestamps || !Array.isArray(timestamps)) { + if (!timestamps || !Array.isArray(timestamps)) { return res.status(400).json({ error: 'Invalid request' }); } + // Validate timestamp count + if (timestamps.length > 256) { + return res.status(400).json({ error: 'Phrase too long (max 256 characters)' }); + } + // Check rate limit const now = Math.floor(Date.now() / 1000); const oneHourAgo = now - 3600; @@ -200,22 +207,7 @@ app.post('/api/submit', (req, res) => { }); } - // Validate phrase - if (phrase.length !== timestamps.length || phrase.length === 0 || phrase.length > 200) { - return res.status(400).json({ error: 'Invalid phrase length' }); - } - - // Check for duplicate phrase - const dupeResult = db.exec( - 'SELECT COUNT(*) as count FROM submissions WHERE LOWER(TRIM(phrase)) = LOWER(TRIM(?))', - [phrase.trim()] - ); - const dupeCount = dupeResult[0]?.values[0]?.[0] || 0; - if (dupeCount > 0) { - return res.status(400).json({ error: 'This phrase already exists on the leaderboard' }); - } - - // Validate all letters in one query + // Resolve phrase from server-side letter records const placeholders = timestamps.map(() => '?').join(','); const letterResult = db.exec( `SELECT timestamp, letter FROM letters WHERE timestamp IN (${placeholders})`, @@ -227,17 +219,25 @@ app.post('/api/submit', (req, res) => { validLetters.set(ts, lt); } } - for (let i = 0; i < timestamps.length; i++) { - if (validLetters.get(timestamps[i]) !== phrase[i]) { - return res.status(400).json({ - error: 'Invalid submission - letters do not match server records' - }); - } + const chars = timestamps.map(ts => validLetters.get(ts)); + if (chars.some(c => c === undefined)) { + return res.status(400).json({ error: 'Invalid submission - unknown timestamps' }); + } + const phrase = chars.join(''); + + // Check for duplicate phrase + const dupeResult = db.exec( + 'SELECT COUNT(*) as count FROM submissions WHERE LOWER(TRIM(phrase)) = LOWER(TRIM(?))', + [phrase] + ); + const dupeCount = dupeResult[0]?.values[0]?.[0] || 0; + if (dupeCount > 0) { + return res.status(400).json({ error: 'This phrase already exists on the leaderboard' }); } // Insert submission db.run('INSERT INTO submissions (phrase, timestamps) VALUES (?, ?)', - [phrase.trim(), JSON.stringify(timestamps)]); + [phrase, JSON.stringify(timestamps)]); const lastIdResult = db.exec('SELECT last_insert_rowid() as id'); const submissionId = lastIdResult[0].values[0][0]; @@ -257,11 +257,12 @@ app.post('/api/submit', (req, res) => { // Get leaderboard app.get('/api/leaderboard', (req, res) => { + const sort = req.query.sort === 'new' ? 'created_at DESC' : 'votes DESC, created_at DESC'; const result = db.exec(` SELECT id, phrase, votes, created_at FROM submissions - ORDER BY votes DESC, created_at DESC - LIMIT 100 + ORDER BY ${sort} + LIMIT 100 `); let submissions = [];