Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/data/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { CatHttpCode } from "../pages/activities/CatHttpCode";
import FlappyBird from "../pages/games/FlappyBird";
import RandomIdentity from "../pages/activities/RandomIdentity";
import word_scramble_icon from "../assets/games/WordScramble/word_scramble.png";
import DontClickBomb from "../pages/games/DontClickBomb";
import TypingTest from "../pages/games/TypingTest";

export const activities = [
Expand Down Expand Up @@ -182,6 +183,11 @@ export const games = [
element: <WordScramble />,
},
{
title: "Don't Click the Bomb",
description: "Click everything except 💣. Gets harder every round.",
icon: "https://img.freepik.com/free-vector/round-black-bomb-realistic-style_52683-15190.jpg?semt=ais_hybrid&w=740&q=80",
urlTerm: "dont-click-the-bomb",
element: <DontClickBomb />,
title: "Typing Test",
description: "Test your typing skills",
icon :"https://typingmentor.com/_next/image/?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fbs3wcomf%2Fproduction%2F9f626374aa1c388a6418a077990771aff53d054f-1200x800.jpg%3Frect%3D0%2C35%2C1200%2C731%26w%3D808%26h%3D492%26fit%3Dcrop%26auto%3Dformat&w=1920&q=75",
Expand Down
134 changes: 134 additions & 0 deletions src/pages/games/DontClickBomb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { useEffect, useMemo, useState } from "react";
import "../../styles/pages/games/DontClickBomb.css";

const SAFE_EMOJIS = ["😀", "😎", "😺", "🦊", "🐶", "🐻", "🍔", "🍩", "🍓", "🌸", "⭐", "💎"];

export default function DontClickBomb() {
const [level, setLevel] = useState(1);
const [score, setScore] = useState(0);
const [gameOver, setGameOver] = useState(false);
const [timeLeft, setTimeLeft] = useState(2.5); // seconds per round, will shrink
const [tick, setTick] = useState(0); // to re-gen grid when timer refreshes

// grid scales with level
const gridSize = Math.min(3 + Math.floor(level / 1.3), 8); // 3..8
const totalCells = gridSize * gridSize;

// number of bombs grows
const numBombs = Math.min(Math.floor(level * 0.7) + 1, 10);


// choose unique bomb positions
const bombIndexes = useMemo(() => {
const set = new Set();
while (set.size < numBombs) {
set.add(Math.floor(Math.random() * totalCells));
}
return Array.from(set);
}, [level, totalCells, numBombs, tick]);

// timer logic: every level you get slightly less time
useEffect(() => {
if (gameOver) return;
setTimeLeft(Math.max(0.9, 2.6 - level * 0.1)); // min ~0.9s
}, [level, gameOver]);

useEffect(() => {
if (gameOver) return;
const interval = setInterval(() => {
setTimeLeft((t) => {
if (t <= 0.1) {
// time up -> lose
setGameOver(true);
return 0;
}
return Number((t - 0.1).toFixed(1));
});
}, 100);
return () => clearInterval(interval);
}, [gameOver]);

function handleCellClick(idx, isBomb) {
if (gameOver) return;
if (isBomb) {
setGameOver(true);
return;
}
// safe
setScore((s) => s + 10 + level * 2);
setLevel((l) => l + 1);
// refresh grid
setTick((t) => t + 1);
}

function handleRestart() {
setLevel(1);
setScore(0);
setGameOver(false);
setTimeLeft(2.5);
setTick((t) => t + 1);
}

return (
<div className="bomb-wrapper hard">
<div className="bomb-header">
<h2>Don&apos;t Click the Bomb 💣</h2>
<p>Click any safe emoji before the timer runs out. More levels = more bombs.</p>
</div>

<div className="bomb-stats">
<div className="bomb-stat">
<span className="label">Level</span>
<span className="value">{level}</span>
</div>
<div className="bomb-stat">
<span className="label">Score</span>
<span className="value">{score}</span>
</div>
<div className={`bomb-timer ${timeLeft < 1 ? "low" : ""}`}>
⏱ {timeLeft.toFixed(1)}s
</div>
{gameOver && (
<div className="bomb-over">💥 Boom! Too slow / bomb clicked.</div>
)}
</div>

<div
className="bomb-grid"
style={{
gridTemplateColumns: `repeat(${gridSize}, minmax(45px, 1fr))`,
}}
>
{Array.from({ length: totalCells }).map((_, idx) => {
const isBomb = bombIndexes.includes(idx);
const emoji = isBomb
? "💣"
: SAFE_EMOJIS[Math.floor(Math.random() * SAFE_EMOJIS.length)];

return (
<button
key={idx}
className={`bomb-cell ${isBomb ? "is-bomb" : "is-safe"} ${
gameOver && isBomb ? "explode" : ""
}`}
onClick={() => handleCellClick(idx, isBomb)}
disabled={gameOver}
>
{emoji}
</button>
);
})}
</div>

<div className="bomb-actions">
<button onClick={handleRestart} className="bomb-btn">
{gameOver ? "Play again" : "Restart"}
</button>
</div>

<p className="bomb-hint">
From level 4: 2 bombs 🧨. From level 8: 3 bombs 😈. Timer also gets shorter.
</p>
</div>
);
}
173 changes: 173 additions & 0 deletions src/styles/pages/games/DontClickBomb.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
.bomb-wrapper {
max-width: 700px;
margin: 2rem auto;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 1.6rem 1.4rem 1.4rem;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.05);
text-align: center;
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.bomb-header h2 {
font-size: 1.6rem;
font-weight: 700;
color: #0f172a;
margin-bottom: 0.4rem;
}

.bomb-header p {
color: #64748b;
font-size: 0.9rem;
margin-bottom: 1.1rem;
}

.bomb-stats {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
align-items: center;
}

.bomb-stat {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 0.5rem 1.1rem;
min-width: 90px;
}

.bomb-stat .label {
font-size: 0.65rem;
text-transform: uppercase;
color: #94a3b8;
letter-spacing: 0.04em;
}

.bomb-stat .value {
font-size: 1.1rem;
font-weight: 600;
color: #0f172a;
}

.bomb-over {
background: #fee2e2;
color: #b91c1c;
padding: 0.35rem 0.7rem;
border-radius: 999px;
font-weight: 500;
}

.bomb-grid {
display: grid;
gap: 0.6rem;
margin-top: 1.1rem;
}

.bomb-cell {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
height: 60px;
font-size: 1.7rem;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
display: flex;
align-items: center;
justify-content: center;
}

.bomb-cell.is-safe:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(37, 99, 235, 0.12);
}

.bomb-cell:disabled {
cursor: not-allowed;
opacity: 0.9;
}

/* bomb click animation */
.explode {
animation: bombBoom 0.4s ease forwards;
}

@keyframes bombBoom {
0% {
transform: scale(1);
background: #fee2e2;
}
50% {
transform: scale(1.3);
background: #fca5a5;
}
100% {
transform: scale(0.9);
background: #fee2e2;
}
}

.bomb-actions {
margin-top: 1.2rem;
}

.bomb-btn {
background: #2563eb;
color: #ffffff;
border: none;
border-radius: 10px;
padding: 0.5rem 1.2rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}

.bomb-btn:hover {
background: #1d4ed8;
}

.bomb-hint {
margin-top: 0.85rem;
color: #94a3b8;
font-size: 0.78rem;
}

@media (max-width: 520px) {
.bomb-grid {
grid-template-columns: repeat(auto-fit, minmax(55px, 1fr)) !important;
}
.bomb-cell {
height: 55px;
font-size: 1.4rem;
}
}
.bomb-wrapper.hard .bomb-grid {
margin-top: 1rem;
}

.bomb-timer {
background: #eff6ff;
border: 1px solid #bfdbfe;
padding: 0.35rem 0.8rem;
border-radius: 999px;
font-weight: 500;
color: #1d4ed8;
display: flex;
align-items: center;
gap: 4px;
}

.bomb-timer.low {
background: #fee2e2;
border-color: #fecaca;
color: #b91c1c;
}

@media (max-width: 520px) {
.bomb-wrapper.hard .bomb-grid {
grid-template-columns: repeat(auto-fit, minmax(45px, 1fr)) !important;
}
}