Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/spooky-developer/llms.txt

Use this file to discover all available pages before exploring further.

The Projects page turns the portfolio showcase into a Halloween game. Four tall arched doors — styled like tombstones — stand side by side in a grid. Visitors are invited to knock on them: clicking a door triggers a 3D rotateY(105deg) CSS transform that swings it open, just like a real door on a hinge. Behind each door is either a treat (a satisfying success story) or a trick (a hard-fought technical challenge overcome). The icon revealed — CandyIcon for treats, GhostIcon for tricks — sets the mood before the project description appears.

The Trick or Treat Mechanic

State Management

Opened door IDs are tracked with a single useState call:
const [openedDoors, setOpenedDoors] = useState([]);
Clicking a door calls a toggle function that adds the ID to the array if it isn’t there yet, or removes it to close the door again:
const toggleDoor = (id) => {
  setOpenedDoors(prev =>
    prev.includes(id)
      ? prev.filter(n => n !== id)
      : [...prev, id]
  );
};

3D Flip Animation

The door panel is a Tailwind-styled <div> with origin-left so it rotates around its left edge like a hinged door. The open/closed state is toggled by conditionally applying the rotate-y-105 class, which maps to rotateY(105deg):
<div
  className={`
    absolute bottom-0 left-1/2 -translate-x-1/2
    w-48 h-72 bg-haunt-tombstone border-4 border-haunt-tombstoneDark
    rounded-t-full origin-left
    transition-transform duration-700 ease-in-out
    cursor-pointer flex flex-col items-center justify-center z-20
    ${isOpen ? 'rotate-y-105' : ''}
  `}
  onClick={() => toggleDoor(project.id)}
>
  <div className="w-8 h-8 bg-haunt-dark rounded-full mb-8" />
  <div className="absolute right-4 top-1/2 w-4 h-4 bg-haunt-moon rounded-full
                  shadow-[0_0_10px_rgba(94,234,212,0.5)]" />
  <span className="font-spooky text-2xl text-haunt-dark mt-auto mb-8">
    Door {project.id}
  </span>
</div>
The transition-transform duration-700 ease-in-out ensures the swing takes 700 ms and eases smoothly. The parent container uses perspective-1000 so the 3D rotation has depth.

Reveal Content

The project content sits behind the door in the same container and becomes visible (opacity-100 z-10) when the door is open, and invisible (opacity-0 z-0) when it is closed:
<div className={`
  absolute inset-4 mt-16 flex flex-col items-center text-center p-4
  transition-opacity duration-500
  ${isOpen ? 'opacity-100 z-10' : 'opacity-0 z-0'}
`}>
  {/* Icon */}
  {project.type === 'trick'
    ? <GhostIcon  className="w-12 h-12 mx-auto text-haunt-moon" />
    : <CandyIcon  className="w-12 h-12 mx-auto text-haunt-pumpkin" />
  }
  <h3 className="text-xl font-bold text-haunt-moon mb-2">{project.title}</h3>
  <p  className="text-sm text-haunt-bone/80">{project.desc}</p>
  <button className="mt-auto px-4 py-2 bg-haunt-moon/20 text-haunt-moon
                     rounded-full text-sm hover:bg-haunt-moon/40 transition-colors">
    View Case
  </button>
</div>
Setting type: 'treat' on a project causes CandyIcon to appear on reveal (animated with a scale pop: scale: [0, 1.2, 1]). Setting type: 'trick' shows GhostIcon instead (animated with a drop-in bounce: y: [20, 0, -10, 0]). Any value other than 'trick' falls through to the CandyIcon branch.

Projects

All four projects are defined in the PROJECTS array in assets/main.js:
#TitleTypeDescription
1E-Commerce PlatformtreatA full-stack shop with Next.js and Stripe. Increased sales by 40%.
2Legacy RefactortrickUntangled 100k lines of jQuery into clean React. A true horror story.
3Design SystemtreatBuilt a comprehensive component library used by 50+ developers.
4Real-time ChattrickWebSockets, Redis, and a lot of race conditions successfully vanquished.

Data Structure

const PROJECTS = [
  { id: 1, title: 'E-Commerce Platform', type: 'treat', desc: 'A full-stack shop with Next.js and Stripe. Increased sales by 40%.' },
  { id: 2, title: 'Legacy Refactor',     type: 'trick', desc: 'Untangled 100k lines of jQuery into clean React. A true horror story.' },
  { id: 3, title: 'Design System',       type: 'treat', desc: 'Built a comprehensive component library used by 50+ developers.' },
  { id: 4, title: 'Real-time Chat',      type: 'trick', desc: 'WebSockets, Redis, and a lot of race conditions successfully vanquished.' },
];
Each object requires four fields:
FieldTypeDescription
idnumberUnique identifier used as the React key and the door toggle ID. Must be unique across all projects.
titlestringProject name shown inside the opened door.
type'treat' | 'trick'Controls which icon is shown on reveal and sets the thematic tone.
descstringOne or two sentence description displayed below the icon.

Adding a New Project

1

Add an entry to PROJECTS

Append a new object to the PROJECTS array in assets/main.js. Give it the next sequential id:
const PROJECTS = [
  // … existing projects …
  {
    id:    5,
    title: 'AI Dashboard',
    type:  'treat',
    desc:  'Built a real-time analytics dashboard with OpenAI streaming responses.',
  },
];
2

Choose a type

Pick 'treat' for a success story or 'trick' for a challenge-overcome narrative. This controls the reveal icon and animation — no other changes are needed.
3

Rebuild and preview

Run the dev server (vite) and navigate to /projects. Your new door will appear automatically in the CSS Grid. The grid is grid-cols-1 md:grid-cols-2 lg:grid-cols-4, so adding a fifth project will cause the grid to reflow — consider adjusting the column count if you add many projects.
The grid uses perspective-1000 on the container to give the 3D door rotation proper depth. If you remove or override this class, the rotateY transform will appear flat.

Build docs developers (and LLMs) love