Bridge
A personal operations dashboard managing chores, fitness, plants, and job applications. Deployed on a home server.
Live Demo
Overview
Dinner With Lucy is a self-hosted recipe manager for my household, covering recipe creation, cookbook collections, and per-user star ratings. It spans 16 routes across four areas: public browsing, authenticated user management, an admin panel for accounts and tags, and the auth flow.
Ingredient scaling
Ingredients are stored as (numerator, denominator) integer pairs rather than decimals. Ingredient::parseQuantity() accepts freeform input ("2 1/4", "1/2") and converts it to the pair. formattedAtScale(int $mult, int $div) multiplies the stored fraction, simplifies with the Euclidean GCD algorithm, then formats it back as a mixed number — so 1/3 at 3× returns exactly "1 cup" rather than "1.0 cup" or floating-point drift.
All four scale variants (½×, 1×, 2×, 3×) are pre-computed server-side and serialised into the Blade output via @js($ingredientScales). Alpine reads the correct variant with a :text binding on each ingredient. The scale toggle is entirely client-side — no server round-trip, and no Livewire re-render that would reset the ingredient checkboxes the user had ticked mid-recipe.
Cookbooks
Cookbooks are ordered collections of recipes. Attachment order is preserved via a sort_order column on the cookbook_recipe pivot, queried with orderByPivot('sort_order'). Cascade constraints on both foreign keys handle cleanup of pivot rows, images, ratings, and tags automatically when a cookbook is deleted.
Star ratings
Ratings are polymorphic (rateable_id, rateable_type) via a HasRatings trait shared between recipes and cookbooks. Clicking the same star again removes the rating — HasRatings::rate() calls updateOrCreate keyed on user and rateable. The star display has two modes: read-only renders a two-layer SVG where the filled layer is clipped to a percentage width matching the average (3.7 stars = 74% fill); interactive mode uses Alpine to track hover state with :class bindings per star, then fires wire:click to persist.
Tags
58 built-in theme tags are seeded from DefaultThemeTags::TAGS with colours defined as CSS variable pairs. Their colours are locked in the admin UI — DefaultThemeTags::isDefault($slug) blocks the colour picker for them. Admin-created custom tags store their colour as inline style attributes. Tags attach via a polymorphic many-to-many; the live-search input in editors calls Tag::firstOrCreate() so typing a new name creates it on save.
Livewire compiler bug
Livewire 4.2.2's component detector uses a greedy regex with the s (dotall) flag to check for class-based components. It spans past the closing PHP tag into the Blade template, so any Volt component whose HTML contained the word "new" — a "new cookbook" button, a "new recipe" link — was misidentified as a class-based component and broke the page. The fix, PatchedLivewireFinder in app/Support/, extracts just the PHP block with a separate regex first, then runs the class-detection check on that substring only. It's swapped into the container in AppServiceProvider.
Theming
Dark and light modes use CSS custom properties rather than Tailwind's dark: variant. The theme is applied via a data-theme attribute on <html>; a [data-theme="dark"] block in app.css reassigns the full set of CSS custom properties. The toggle is a plain Alpine button in the nav that writes to localStorage under the key dwl-theme and updates the attribute directly with document.documentElement.setAttribute('data-theme', theme) — no server involvement. An inline <script> in <head> reads localStorage and sets the attribute before first paint, and re-runs on livewire:navigated to survive Livewire's SPA-style navigation. First-time visitors get the light theme by default — there's no prefers-color-scheme detection.
Architecture
Every page is a Livewire Volt functional component — state(), mount(), computed(), and action() declarations in a single .blade.php file, routed via Volt::route(). No traditional controllers. Business logic touching the filesystem lives in ImageService; cross-model behaviours are in HasImages and HasRatings traits.
Explore
A personal operations dashboard managing chores, fitness, plants, and job applications. Deployed on a home server.