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 = [];