Inspiration
"Mankind was my business." Today, what is the true business of your day?
We have all been obsessed about productivity and efficiency and when the Kiroween challenge asked for something spooky, I wondered: What if productivity itself was the horror story?
Drawing inspiration from Charles Dickens' A Christmas Carol, I found the connection of Halloween and Christmas themes and perfect metaphor to bind them. Just as Jacob Marley was weighed down by chains forged from a lifetime of neglect, every task we abandon is another link in our own chains. Every task we complete is a soul saved.
The result is 'Marley's Ledger'—a to-do app where tasks literally haunt you, and your productivity determines whether you are redeemed or condemned.
What It Does

"We must travel with the Spirits. Let us attend to your business."
Marley's Ledger isn't just a to-do list; it's a haunting. Commit to your work, or face the weight of your own procrastination:
- Orbital Haunting: Your tasks don't sit; they haunt. To-dos circle your screen as visual ghosts that demand attention.
- Chains of Neglect: Subtasks are the fetters you forge. Unfinished items tighten the chains; completing them shatters the links.
- Soul Tracking: Judge your own worth. The 'Saved vs. Lost' scale weighs your productivity instantly.
- 'Consult the Spirits': Don't plan alone. Our Vercel-powered AI whispers specific, actionable guidance to guide you through the fog.
- Tonight's Ledger: Face your judgment. End the day with a thematic verdict: 'Your chains grow lighter. Marley would be proud'.
How We Built It
I moved beyond "vibe coding" into spec-driven development. Before writing a single line of code, I established a strict plan using the AI IDE Kiro.
- 10 User Stories with formal acceptance criteria.
- 12 Correctness Properties for property-based testing.
- 14 Implementation Phases to ensure steady progress.
The Stack
- Framework: Next.js 16 + React 19
- Language: TypeScript
- Styling: Tailwind CSS 4 (Victorian palette: gold, deep purple, coral, pale pink)
- State: Zustand
- Testing: Vitest + fast-check
The Hydration Haunt
After deployment, the project immediately hit the dreaded React Error #185. This hydration mismatch appeared strictly in production, threatening to derail the launch.
The Problem
Initially, I used a standard React Context approach with useEffect to load data from localStorage on mount.
In development, this worked fine. However, in production, the server rendered the initial HTML (an empty state), but the client immediately replaced it with data from localStorage. Next.js detected the difference between the server HTML and the first client paint, triggering the crash.
The Code That Broke Production
// The original TaskContext.tsx
export function TaskProvider({ children }: { children: ReactNode }) {
const [tasks, setTasks] = useState<Task[]>([])
// 🚩 THIS CAUSED THE HYDRATION ERROR
// Server renders [], Client immediately loads storage and re-renders
useEffect(() => {
const state = loadState()
setTasks(state.tasks) // ← Hydration mismatch!
}, [])
}
The Fix: Zustand + SkipHydration
After struggling to debug the issue manually, I consulted Kiro. It suggested migrating from Context to Zustand with the persist middleware.
The key was using skipHydration. This forces the store to wait until the client has fully mounted before reconciling the state, ensuring the server and client HTML match exactly on the first paint.
The Store Configuration:
// src/stores/taskStore.ts
export const useTaskStore = create<TaskState>()(
persist(
(set, get) => ({
tasks: [],
savedSouls: 0,
lostSouls: 0,
}),
{
name: 'marleys-ledger-state',
skipHydration: true, // ← Key: Don't auto-hydrate on SSR
}
)
)
The Hydration Component:
We added a component to handle the sync manually after the mount:
// src/stores/StoreHydration.tsx
'use client'
import { useEffect } from 'react'
import { useTaskStore } from './taskStore'
export default function StoreHydration() {
useEffect(() => {
useTaskStore.persist.rehydrate() // ← Hydrate only on client
}, [])
return null
}
This refactor took minutes, removed complex providers, and completely solved the SSR issues.
Accomplishments & Wins
- 45 Property-Based Tests: Kiro caught complex logic bugs—like broken chain visibility states—before writing a single line of UI code. The tests identified edge cases I never would have found manually.
- The Metaphor Hits Hard: The ghost/chain/soul system isn't just decoration; it reframes the psychology of work. Abandoning a to-do item feels significantly heavier when you see "Lost Soul +1" flash on the screen.
- AI-Assisted Architecture: Instead of scouring documentation for hours, I described the symptom to Kiro, and it immediately proposed the architecture shift to Zustand, turning a panic moment into a five-minute fix.
What's Next
- Sound Design: Adding atmospheric audio, such as chain rattles when adding tasks and bell chimes for saved souls.
- Multiplayer Haunting: A feature allowing you to share ledgers with friends and see each other's ghosts.
- Mobile App: A React Native version so your ghosts can follow you offline.
Thanks for checking out Marley's Ledger! If you want to see if your productivity is redeemed or condemned, check out the repo below.
Marley's Ledger
If you have any questions about the technical choices, the Dickensian themes, or why I chose to build Marley's Ledger, please leave them in the comments below! I'd love to spark additional discussion and explain why I personally found this project helpful (and hopefully why you might, too).

Top comments (0)