A cozy website for my plushie collection, where each plushie has its own page with a name, birthday, favorite things and a photo gallery. Visitors can sign in to like plushies and leave comments, and a dashboard lets editors and admins look after the collection and the community.
Stack: Next.js 16 (App Router, server actions), React 19, TypeScript, Tailwind CSS 4, shadcn/ui, Prisma on PostgreSQL, Better Auth, UploadThing, Resend, Bun
Reverts that can't destroy newer work. Each activity entry stores the relevant fields from before and after the change. A revert is only offered while the current state still matches the entry's "after". If anything has changed since, the button goes away instead of silently overwriting that change.
Security at every step. Deleting an account or changing a verified email needs a link from the current inbox, so a stolen session isn't enough. Discord tokens are encrypted at rest. Sign-in codes are stored only as hashes and have their own rate limit. Password reset requests take the same time whether or not the email exists. Viewing the site as someone else is strictly read-only, enforced on the server. The page that tells a banned person why they were banned only opens through a signed, short-lived cookie, so nobody else can look up someone's ban.
Guardrails for admins. The last admin can't delete their account and leave the site without one. Admins can't be banned. The auth library's own admin endpoints are narrowed to only what the site actually offers.
Tests against a real database. 75 tests cover plushies and photos, comments, likes, user management, bans, reverts, two-step sign-in and account settings. Each run starts a throwaway PostgreSQL container, applies the real migrations and removes the container afterwards. The test setup refuses to run against any database other than a local one, emails are recorded instead of sent, and file uploads go to a pretend store.
Polish. The loading placeholders match the shape of the real pages. Empty states are designed, not blank. Forms keep what you typed when saving fails. Text in menus is trimmed to cap height so it sits centered.