A Laravel 13 + Inertia + Vue 3 + Tailwind v4 app for browsing a large, fully-seeded events dataset (~1.25M rows) two distinct ways — an editorial magazine layout and an interactive month calendar — with local images, human-readable addresses, timezone-aware times, date/location filtering, and an attendee registration + email reminder system. Styled with a warm "print" editorial theme (Fraunces display serif).
Built as a solution to
CODING_TEST.md. See DECISIONS.md for the reasoning behind the design, EXPLAIN.md for query/index evidence, and RUNBOOK.md for local setup/troubleshooting commands.
- Two browsing experiences —
/events-visual-1(editorial magazine: hero feature + asymmetric mosaic, infinite scroll) and/events-visual-2(month calendar: per-day event chips with a synced day agenda, month-by-month navigation). - Images, end to end —
event_imagestable + relationship, 2 images per event, served locally fromstorage/app/public/events(no hotlinking). - Addresses from lat/lng — offline nearest-city resolver → "Near {City}, {Country}".
- Timezones — event-local time shown alongside the viewer's local time.
- Filtering — by date range, city, and category (magazine); by month, city, and category (calendar — the month navigation is the date filter).
- Attendees & email — register interest, get a queued confirmation email, and reminder emails 3 days and 24 hours before the event.
- Built for scale — composite indexes, lean API payloads, a per-month capped calendar feed.
- PHP 8.3+ (with
pdo_sqlitefor the test suite,pdo_mysqlfor the app DB) - Node 20+, MySQL 8+
composer install
npm install
cp .env.example .env
php artisan key:generate
# point DB_* at your MySQL instance (defaults: db lhp_coding, root/password)
php artisan migrate
php artisan storage:link # serve the local event images
# Seed the dataset. Full is ~1.25M events (a few minutes, ~2 GB):
php artisan db:seed
# ...or create a medium local demo seed with events, city-anchored locations,
# and two local images per event:
php artisan db:seed --class=PartialEventSeeder
# Override the medium size while iterating:
MEDIUM_SEED_ROWS=5000 php artisan db:seed --class=PartialEventSeeder
# If you already have events and only need to backfill image rows:
php artisan db:seed --class=EventImageSeedercomposer run dev # serve + queue worker + vite + log tail, all at onceor individually:
php artisan serve
php artisan queue:work # processes confirmation/reminder emails
npm run devThen open http://localhost:8000/events-visual-1 and /events-visual-2.
Mail uses the log driver by default, so confirmation/reminder emails land in
storage/logs/laravel.log — handy for the demo.
Reminders are queued by a scheduled command:
php artisan events:send-reminders # run once (idempotent)
php artisan schedule:work # run the hourly scheduler locallyIn production, point cron at php artisan schedule:run every minute as usual.
composer test # Pint + PHPStan (level 7) + Pest
npm run lint:check && npm run format:check && npm run types:checkThe test suite is configured for SQLite in-memory (standard). If your environment
lacks pdo_sqlite, you can run against a throwaway MySQL DB without changing config:
mysql -uroot -ppassword -e "CREATE DATABASE IF NOT EXISTS lhp_coding_test;"
DB_CONNECTION=mysql DB_DATABASE=lhp_coding_test php artisan test| Area | Path |
|---|---|
| Models | app/Models/{Event,EventImage,Attendee}.php |
| Offline geocoding | app/Services/LocationResolver.php, config/cities.php |
| API shaping | app/Services/EventTransformer.php |
| Controllers | app/Http/Controllers/{EventController,AttendeeController}.php |
app/Mail/*, app/Console/Commands/SendEventReminders.php |
|
| Indexes / schema | database/migrations/2026_06_24_* |
| Pages | resources/js/pages/Events/{VisualOne,VisualTwo,Show}.vue |
| Shared UI | resources/js/components/{EditorialTile,EventFilters}.vue, composables/useEventDateTime.ts |
| Theme | resources/css/app.css (editorial palette + --brand), vite.config.ts (Fraunces) |