Dinner With Lucy
A recipe and cookbook manager with fraction-accurate ingredient scaling, star ratings, and cookbook collections.
Screenshots
Overview
The Bridge is a household operations dashboard I built to manage daily logistics: chore rotations, fitness tracking, plant care schedules, and job application pipelines. It's self-hosted on a machine running in the house, accessible from any device on the LAN via Docker. The app spans 25 pages across eight areas — chores, exercises (6 pages), plants, applications, equipment, settings, admin, and auth.
The starting point was replacing a collection of scattered apps and spreadsheets with one tool built around how we actually operate.
Recurrence engine
A scheduled command runs at midnight and builds the day automatically. Chore recurrence is driven by a custom isDueOn(Carbon $date): bool method on the Chore model. Each chore stores a rule string as a plain VARCHAR — five variants parsed entirely with str_starts_with and explode, no regex, no external library: daily, weekly:mon,wed,fri, biweekly:mon, monthly:15 / monthly:last, and interval:7. Biweekly chores are anchored to a start date stored on the record; the algorithm counts full ISO weeks between anchor and target with diffInWeeks() — if the result is even, the chore is due. Because isDueOn() is stateless and recomputed fresh each day, a missed or late chore never causes the schedule to drift.
Completing a plant-watering chore triggers a side-effect chain in ChoreCompletion::complete(): it creates a WateringLog, calls Plant::recalculateNextWaterDate() to add the plant's frequency to the latest watering timestamp, and writes the result back to a stored next_water_date column — all in one place, branching on the chore's source_type. Reads are instant; recomputation only happens on each completion.
Fitness tracking
Workout logging is paired with a pair of line charts on the exercises dashboard. The strength progression chart offers a selectable exercise (filtered to only what the user has actually logged), a 30d/90d/1yr time range, and a metric selector that adapts to the exercise category: weightlifting exposes Max Weight, Total Weight, Total Sets, and Total Reps; bodyweight exposes Max Reps and Total Reps; cardio and stretching expose Total Duration. Volume ("Total Weight") is computed server-side as sum(weight × reps) across all sets for the day, with per-point annotations carried in an extra[] array for Chart.js tooltip context. The body weight chart uses an integer $weightChartVersion counter incremented on each save and embedded in wire:key, which forces Chart.js to destroy and recreate the canvas rather than animate to the new datapoint.
The exercise log page includes a client-side rest timer with pause, resume, and restart. Timer durations are persisted to a user_timers table and the five most-recently-used unique values surface as quick-picks — surviving across sessions and devices.
Livewire/Alpine race condition
Building the rest timer exposed a subtle interaction between Alpine and Livewire's morphdom reconciliation. Starting the timer updates Alpine's state, then calls $wire.startTimer(), which invalidates the quick-picks list and triggers a server round-trip. The problem: morphdom was patching the duration input back to its last server-rendered state mid-countdown, resetting what the user had just set. The fix required three coordinated changes: wire:key on the timer container to give morphdom a stable identity, wire:ignore on the input wrapper to exclude it from diffing entirely, and a 50ms delay before the server call to let Alpine settle first. Any one of the three in isolation wasn't enough.
Plant photo upload
Plant profile photos use a custom Alpine.js crop UI. The preview renders the image with CSS object-cover inside a square container; the crop selection is tracked as center percentages and a size percentage relative to that rendered square. Before saving, the server explicitly replicates the object-cover geometry: it computes coverOffsetX and coverOffsetY from the difference between the original dimensions and the short side, then maps the UI percentages back to source pixel coordinates before cropping with GD. A landscape and a portrait photo with identical UI crop positions produce identical-composition results.
Theming
Dark and light modes use CSS custom properties, not Tailwind's dark: variant. The preference is stored per user and applied on load. Swapping one class on <html> updates every color in the UI.
Component architecture
Every application page is a Livewire Volt single-file component — PHP class and Blade template in one file, routed directly via Volt::route() with auth middleware. There are zero hand-written controllers for application logic; the only controllers in the project are Breeze's generated auth scaffolding. Common patterns (stat cards, data tables, confirmation modals) live in shared components with well-defined properties and slots.
Job applications
The application tracker stores company, role, job type, contact details, and notes per application. Soft deletes are surfaced as a first-class UI state: a "Deleted" filter tab switches the base query to onlyTrashed() and exposes a Restore action, so the deleted view is a recovery surface rather than a safety net. Filter and search state are both #[Url] attributes — any filter/search combination is deep-linkable and survives page refresh.
Admin panel
Widget visibility operates on two independent layers: a global show flag per widget (admin-controlled) and a per-user UserWidget.show flag (user-controlled via settings). Admins flipping a widget off globally overrides user preference. User::isWidgetVisible(string $name) resolves both layers in one call. The nightly scheduler runs four commands: generating watering chores for plants due today, generating and carrying forward recurring chores, auto-completing abandoned workout logs (and purging orphaned empty logs), and a monthly hard-purge of soft-deleted exercise sets. The carry-forward logic uses a "most recent prior completion" check to avoid re-creating chores that have already been finished.
Explore
A recipe and cookbook manager with fraction-accurate ingredient scaling, star ratings, and cookbook collections.
A standalone PHP 8.2+ library that evaluates whether a recurring rule string is due on a given date — extracted from Bridge and published on Packagist.