This is a submission for the DEV Weekend Challenge: Earth Day Edition: Build for the Planet
Why I Built This
Every April 22nd, Earth Day reminds us that the planet needs more than good intentions โ it needs informed action. One of the most impactful things any individual can do is sort their waste correctly. Yet most people genuinely don't know whether a greasy pizza box goes in paper recycling, whether broken glass belongs with regular glass, or whether a CFL light bulb is hazardous waste.
I wanted to build something that makes learning those answers feel like play, not homework. So I built the Earth Day Waste Sorting Challenge โ a drag-and-drop educational game that teaches real recycling science through fast-paced gameplay, environmental fact cards, and a live impact tracker that shows you exactly how much waste you're diverting from landfill.
Table of Contents
- What I Built
- Live Demo
- The Earth Day Experience
- How the Game Works
- The Impact Tracker โ Making It Feel Real
- Trick Items โ Challenging What You Think You Know
- The Eco_Score System
- Five Game Modes for Every Player
- Building It: Tech Stack and Architecture
- The Spec-Driven Approach
- Property-Based Testing for Correctness
- Accessibility First
- What I Learned
- Conclusion
What I Built
The Earth Day Waste Sorting Challenge is a React 18 / TypeScript web game where players drag waste items into six color-coded bins: โป๏ธ Plastic, ๐ Paper, ๐ Organic, ๐บ Glass, ๐ E-Waste, and โ ๏ธ Hazardous.
What makes it different from a simple quiz:
- 60+ real waste items with factually accurate environmental facts (decomposition times, COโ figures, recycling science)
- Live Impact Tracker showing estimated kilograms of waste diverted from landfill and COโ equivalent saved โ in real time, as you sort
- Earth Day Mode that activates automatically on April 22nd each year, with a curated 30-item challenge and a countdown to midnight
- Fact Cards after every sort โ correct or wrong โ so every interaction is a learning moment
- Five game modes from casual Zen sorting to competitive Survival with lives
- Full accessibility support including keyboard navigation, ARIA labels, and a high-contrast mode
Live Demo
mohamednizzad
/
Waste-Sorting-Challenge
An educational drag-and-drop waste sorting game built to celebrate Earth Day (April 22nd) and deepen players' understanding of real-world recycling, environmental impact, and sustainable habits.
๐ Earth Day Waste Sorting Challenge
Sort Smarter. Save the Planet.
An educational drag-and-drop waste sorting game built to celebrate Earth Day (April 22nd) and deepen players' understanding of real-world recycling, environmental impact, and sustainable habits.
๐ฎ What Is This?
The Earth Day Waste Sorting Challenge is a fast-paced, educational web game where players drag waste items into the correct recycling, organic, or hazardous bins. Every correct sort teaches a real environmental fact and tracks your estimated real-world impact โ kilograms of waste diverted from landfill and COโ equivalent saved.
The game features a special Earth Day Mode that activates on April 22nd each year, a curated library of 60+ waste items, five distinct game modes, and a full impact tracker that makes every sort feel meaningful.
โจ Features
๐ฑ Earth Day Theme
- Earth-green color palette (
#2d6a4f,#52b788) with parallax scrolling background - Animated ๐ globe and "Sortโฆ
The Earth Day Experience
The game is designed to feel like an Earth Day celebration from the moment you open it.
The home screen greets you with an animated ๐ globe, the tagline "Sort Smarter. Save the Planet.", and a rotating "Did You Know?" panel cycling through 10 curated Earth Day facts every 5 seconds. The color palette โ deep greens (#2d6a4f, #52b788), ocean blues (#0077b6, #90e0ef), and earth tones โ sets the mood immediately.
On April 22nd, the experience transforms. Animated falling leaves drift across the screen, a "๐ Happy Earth Day!" banner appears, and a special Earth Day Mode button becomes available. Earth Day Mode presents a curated set of 30 waste items chosen specifically for their real-world environmental impact โ the items that matter most to get right. A countdown timer shows how many hours and minutes remain until midnight, creating a gentle sense of occasion.
On every other day, the home screen shows a "Next Earth Day" countdown โ a small but persistent reminder that April 22nd is coming, and that the planet is always worth thinking about.
The Earth Day detection is isolated behind a single injectable getNow() function:
function useEarthDay(getNow?: () => Date): UseEarthDayReturn {
const now = (getNow ?? (() => new Date()))();
const isEarthDay = now.getMonth() === 3 && now.getDate() === 22;
// ...
}
This makes the hook trivially testable โ tests inject a fixed date, production uses new Date().
How the Game Works
The core loop is simple and satisfying:
- A waste item card appears in the Action Area โ for example, "โ Coffee Grounds"
- You drag it to one of the six bins
- The game validates your choice and shows a Fact Card
- The next item spawns
The Fact Card is where the real learning happens. For a correct sort, it confirms your choice and shares an environmental fact: "Coffee grounds add nitrogen to compost and can even deter pests from gardens." For a wrong sort, it explains the correct category and why โ and for counterintuitive "trick" items, it leads with "Did you know?" to signal that the answer is surprising.
// FactCard auto-dismiss timing
const dismissDelay = isZenMode ? null : (isCorrect ? 1500 : 2500);
Correct sorts dismiss after 1.5 seconds so the game stays fast. Wrong sorts stay for 2.5 seconds โ long enough to actually read the fact. In Zen mode, the card stays until you tap "Got it โ".
The Impact Tracker โ Making It Feel Real
The most important design decision in this project was the Impact Tracker.
Points and combos are fun, but they're abstract. What makes the game feel genuinely meaningful is watching real numbers change: "You've diverted 2.3 kg of waste from landfill and saved 1.1 kg COโe."
Every waste item in the game data carries two values:
interface WasteItem {
// ...
impactKg: number; // kg of waste diverted per correct sort
co2eSaved: number; // kg COโe saved per correct sort
}
These are based on real-world recycling impact data. A correctly sorted aluminum can saves roughly 0.17 kg COโe. A recycled glass bottle diverts about 0.4 kg from landfill. The numbers are estimates, but they're grounded in published environmental research.
The accumulation logic is a pure function:
function accumulateImpact(
items: WasteItem[],
correctSortIds: string[]
): { totalImpactKg: number; totalCo2eSaved: number } {
return correctSortIds.reduce((acc, id) => {
const item = items.find(i => i.id === id);
if (!item) return acc;
return {
totalImpactKg: acc.totalImpactKg + item.impactKg,
totalCo2eSaved: acc.totalCo2eSaved + item.co2eSaved,
};
}, { totalImpactKg: 0, totalCo2eSaved: 0 });
}
The Planet Health Bar fills from 0% to 100% as your correct sorts accumulate. When it hits 100%, the screen erupts in green confetti and displays: "You've made a real difference today! ๐"
The results screen closes the loop with a concrete sentence: "You properly sorted 23 items, diverting an estimated 4.6 kg of waste from landfills and saving 2.1 kg COโe!"
Trick Items โ Challenging What You Think You Know
The most educational moments in the game come from the "trick" items โ waste items whose correct category is counterintuitive.
| Item | What Most People Think | Correct Answer | Why |
|---|---|---|---|
| ๐ Greasy Pizza Box | Paper recycling | Organic / Trash | Grease contaminates paper recycling; the greasy portion must be composted or trashed |
| ๐ฅ Broken Glass | Glass recycling | Hazardous | Broken glass is dangerous for sanitation workers and can't be processed with whole glass |
| ๐ก CFL Light Bulb | General trash | Hazardous | Contains mercury โ must be processed at a hazardous waste facility |
| ๐ต Tea Bag | Organic | Organic (check first!) | Most are compostable, but some contain plastic mesh โ always check |
| ๐งด Bleach Bottle (partial) | Plastic recycling | Hazardous | Chemical residues make partial containers hazardous |
When a trick item is sorted incorrectly, the Fact Card leads with "Did you know?" โ a signal that the answer is surprising and worth paying attention to.
const factText = item.isTrick && !isCorrect
? `Did you know? ${item.fact}`
: item.fact;
These items are the ones players remember. They're the moments where the game changes someone's actual behavior.
The Eco_Score System
The scoring system is designed to reward both accuracy and environmental awareness:
Eco_Score = floor(base_points ร combo_multiplier) + speed_bonus + impact_bonus
- Base points: 10 (difficulty 1) / 15 (difficulty 2) / 25 (difficulty 3)
- Combo multiplier: ร1.0 โ ร1.2 โ ร1.5 โ ร2.0 for streaks of 1โ2, 3โ4, 5โ9, 10+
- Speed bonus: +5 pts for sorts under 2 seconds (not in Zen mode)
-
Impact bonus:
floor(impactKg ร 10)โ items with higher real-world impact are worth more points - Wrong sort: โ5 pts (minimum 0), combo resets
The impact bonus is the key design choice here. It means that correctly sorting a lithium battery (high environmental impact) scores more than sorting a newspaper (lower impact). The scoring system teaches players which items matter most.
All scoring logic lives in pure functions with no side effects:
export function calculateEcoScore(params: {
difficulty: 1 | 2 | 3;
combo: number;
responseTimeSeconds: number;
impactKg: number;
gameMode: GameMode;
}): number {
const base = getBasePoints(params.difficulty);
const mult = getComboMultiplier(params.combo);
const speed = getSpeedBonus(params.responseTimeSeconds, params.gameMode);
const impact = getImpactBonus(params.impactKg);
return Math.floor(base * mult) + speed + impact;
}
Five Game Modes for Every Player
| Mode | Best For | Mechanic |
|---|---|---|
| Classic | Competitive players | 60-second timer, maximize score |
| Time Attack | Speed runners | 20 items; correct sorts +3s, wrong sorts โ5s |
| Survival | Challenge seekers | 3 lives; mistakes and slow sorts cost a life |
| Zen | Learners, beginners | No timer, no penalties, facts stay until dismissed |
| Earth Day | April 22nd | Curated 30-item challenge with midnight countdown |
Zen mode is particularly important for the educational mission. Without time pressure, players actually read the Fact Cards. They learn. The game becomes a reference tool rather than a reflex test.
Building It: Tech Stack and Architecture
The game is built on React 18 / TypeScript / Tailwind CSS / @dnd-kit / framer-motion โ the same stack as the original prototype, extended without breaking any existing contracts.
Architecture Principles
Additive, not destructive. Every change extends existing types and components rather than replacing them. The original WasteGame, DraggableItem, and DroppableBin components remain intact; new capabilities are layered on top.
Pure-function core. Scoring, impact calculation, level gating, and serialization are all pure functions. No side effects, no React state โ just inputs and outputs. This makes them trivially testable and reusable.
src/
โโโ logic/ # Pure functions (scoring, levelGating, impactCalc, serializer)
โโโ hooks/ # React hooks (useGameEngine, useEarthDay, useAccessibility)
โโโ components/ # UI components
โโโ data/ # Game data and Earth Day static content
Date-aware but mockable. Earth Day detection is isolated behind a single injectable getNow() function. Tests inject a fixed date; production uses new Date(). No global mocking required.
Serialization-safe. All session state serializes to/from JSON with full round-trip fidelity. The deserializer never throws โ it always returns either a valid state or a descriptive error object.
type DeserializeResult =
| { ok: true; state: GameSessionState }
| { ok: false; error: string };
function deserializeSession(json: string): DeserializeResult {
try {
const parsed = JSON.parse(json);
// validate required fields...
return { ok: true, state: parsed };
} catch (e) {
return { ok: false, error: String(e) };
}
}
Drag and Drop
@dnd-kit handles all drag-and-drop interactions with full touch support. The MouseSensor and TouchSensor are configured with activation constraints to prevent accidental drags:
const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { distance: 10 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 100, tolerance: 5 } })
);
Draggable items use touch-none and select-none to prevent mobile browser scroll interference during drag operations.
Responsive Layout
The bins grid uses CSS auto-fit to collapse to 2 columns on narrow viewports and expand to 6 on wide ones:
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
All font sizes and spacing use clamp() for fluid scaling without fixed breakpoints.
The Spec-Driven Approach
This project was built using a spec-driven development workflow. Before writing a single line of implementation code, I created three documents:
- Requirements โ 13 requirements with user stories and acceptance criteria in EARS format
- Design โ Architecture diagrams, component interfaces, data models, and 18 formal correctness properties
- Tasks โ 21 implementation tasks with requirement traceability
The correctness properties in the design document are the bridge between human-readable requirements and machine-verifiable guarantees. For example:
Property 3: Session serialization round-trip
For any validGameSessionStateobject,deserializeSession(serializeSession(state).json)SHALL return{ ok: true, state: s }wheresis deeply equal to the original state.
Having these properties written down before implementation meant I knew exactly what "correct" looked like before I started coding. It also meant the property-based tests wrote themselves.
Property-Based Testing for Correctness
The project uses fast-check for property-based testing alongside Vitest. Instead of writing specific test cases, property tests generate hundreds of random inputs and verify that invariants hold for all of them.
Property 1: Eco_Score formula correctness
fc.assert(fc.property(
fc.integer({ min: 1, max: 3 }), // difficulty
fc.nat(50), // combo
fc.float({ min: 0, max: 30 }), // responseTime
fc.float({ min: 0, max: 5 }), // impactKg
fc.constantFrom('classic', 'survival', 'timeattack', 'zen', 'earthday'),
(difficulty, combo, responseTime, impactKg, gameMode) => {
const result = calculateEcoScore({ difficulty, combo, responseTime, impactKg, gameMode });
const expected =
Math.floor(getBasePoints(difficulty) * getComboMultiplier(combo))
+ getSpeedBonus(responseTime, gameMode)
+ getImpactBonus(impactKg);
return result === expected;
}
), { numRuns: 200 });
Property 2: Score never goes negative
fc.assert(fc.property(
fc.array(fc.boolean(), { minLength: 1, maxLength: 100 }),
(correctness) => {
let score = 0;
let combo = 0;
for (const isCorrect of correctness) {
if (isCorrect) {
score += calculateEcoScore({ difficulty: 1, combo, responseTime: 5, impactKg: 0.1, gameMode: 'classic' });
combo++;
} else {
({ score, combo } = applyIncorrectSort(score));
}
}
return score >= 0;
}
), { numRuns: 200 });
18 properties in total cover scoring, serialization, impact accumulation, level gating, planet health bar clamping, performance ratings, Earth Day countdown ranges, and ARIA label formats.
Property-based testing found two bugs during development that example-based tests would have missed โ both edge cases in the combo multiplier boundary conditions.
Accessibility First
Accessibility wasn't an afterthought โ it's a first-class feature.
Accessibility Mode (toggled from settings) applies:
- Font sizes increased by 25%
- Touch targets enlarged to 64ร64px minimum
- High-contrast color palette
- All parallax and non-essential animations disabled, replaced with simple fades
[data-accessibility] * {
font-size: calc(1em * 1.25) !important;
min-width: 64px;
min-height: 64px;
animation: none !important;
transition: opacity 0.2s ease !important;
}
Keyboard navigation lets players sort without a mouse or touch screen: Tab/arrow keys cycle through bins, Enter or Space confirms a sort.
ARIA labels on every interactive element:
- Bins:
aria-label="Plastic recycling bin" - Items:
aria-label="Water Bottle โ drag to sort"
These are verified by property tests (Properties 17 and 18) that render every bin and item and assert the label format.
What I Learned
1. Spec-driven development pays off. Writing requirements and a design document before coding felt slow at first. But when I started implementing, I never had to stop and ask "what should this do?" The spec answered every question. The property tests wrote themselves from the correctness properties.
2. Pure functions are a superpower. Isolating all game logic into pure functions (scoring.ts, levelGating.ts, impactCalc.ts) made testing trivial and debugging fast. When a scoring bug appeared, I knew exactly which file to look in.
3. Property-based testing finds bugs example tests miss. I wrote 18 property tests and they found 2 bugs that my hand-written examples didn't cover. The combo multiplier boundary at exactly combo = 5 behaved differently than I expected. fast-check found it in seconds.
4. The educational content is the hard part. Writing 60+ unique waste item facts, each at least 20 words and factually accurate, took longer than any single component. Getting the trick items right โ making sure the "Did you know?" moments are genuinely surprising and correct โ required real research.
5. Making impact feel real changes behavior. The most common feedback from playtesters was about the Impact Tracker. Seeing "2.3 kg diverted from landfill" made people want to keep playing. Abstract points don't do that. Real numbers do.
Conclusion
The Earth Day Waste Sorting Challenge started as a simple drag-and-drop game and became something I'm genuinely proud of โ a tool that teaches real recycling science through play, makes environmental impact feel tangible, and celebrates Earth Day in a way that lasts beyond April 22nd.
Every time someone learns that a greasy pizza box doesn't go in paper recycling, or that broken glass is hazardous waste, or that a correctly sorted aluminum can saves measurable COโ โ that's a small win for the planet.
The game is open source. If you want to add more waste items, improve the educational facts, or translate it for your community, pull requests are very welcome.
Sort Smarter. Save the Planet. ๐
Navigation
- Why I Built This
- What I Built
- Live Demo
- The Earth Day Experience
- How the Game Works
- The Impact Tracker
- Trick Items
- The Eco_Score System
- Five Game Modes
- Tech Stack and Architecture
- The Spec-Driven Approach
- Property-Based Testing
- Accessibility First
- What I Learned
- Conclusion
Built with โค๏ธ for the Earth Day Challenge 2026


Top comments (1)
Appreciate your way of writing.