webmintydotcom / laravel-quickstart-dashboard-inertia
Laravel starter kit with common addons, including Intertia and a dashboard skeleton
Package info
github.com/webmintydotcom/laravel-quickstart-dashboard-inertia
Language:TypeScript
Type:project
pkg:composer/webmintydotcom/laravel-quickstart-dashboard-inertia
Requires
- php: ^8.4
- inertiajs/inertia-laravel: ^3.0
- intervention/image: ^4.3
- laravel/fortify: ^1.39
- laravel/framework: ^13.0
- laravel/tinker: ^3.0.0
- spatie/laravel-data: ^4.17
- spatie/laravel-ray: ^1.40
- spatie/pest-expectations: ^1.11
- tightenco/ziggy: ^2.6
- vinkla/hashids: ^14.0
- webmintydotcom/laravel-person-name: ^1.0
Requires (Dev)
- driftingly/rector-laravel: ^2.1
- fakerphp/faker: ^1.23
- larastan/larastan: ^3.6
- laravel/pail: ^1.2.2
- laravel/pao: ^1.0
- laravel/pint: ^1.24
- laravel/sail: ^1.41
- mockery/mockery: ^1.6
- nunomaduro/collision: ^8.6
- pestphp/pest: ^5.1
- pestphp/pest-plugin-browser: ^5.0
- pestphp/pest-plugin-faker: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
- pestphp/pest-plugin-type-coverage: ^5.0
- webminty/boost-webminty-guidelines: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Introduction
A starter kit for Laravel applications with React, Inertia.js, Shark UI, and Tailwind CSS v4. Designed to help you quickly set up a new full-stack Laravel project with a modern development environment.
Out of the box it ships Fortify-backed authentication screens, an authenticated app shell (sidebar, top bar, light/dark/system appearance), a Settings page, a Profile page, and two removable demo pages: a dashboard that shows every panel state a data-driven page needs, and a vehicle list, detail and edit flow that shows real server-side search, filtering, sorting and pagination. See Application Shell for the details.
Installation
laravel new my-app --pest --npm --using=webmintydotcom/laravel-quickstart-dashboard-inertia
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Laravel 13, PHP 8.4 |
| Frontend | React 19, TypeScript |
| Routing | Inertia.js |
| Auth | Laravel Fortify |
| UI Components | Shark UI (Ark UI) |
| Styling | Tailwind CSS v4 |
| Build | Vite 8 |
| Testing | Pest, Larastan |
Included Packages
Laravel
Version 13 of Laravel is used in this starter kit.
Inertia.js
Inertia.js connects the Laravel backend to the React frontend without needing an API. Pages are React components rendered via Inertia::render() in your routes and controllers.
Ziggy
Ziggy provides a route() helper in JavaScript, so you can use Laravel named routes in your React components.
Spatie Laravel Data
Spatie Laravel Data is included to help you create data transfer objects (DTOs) in a simple and elegant way.
Laravel Fortify
Fortify is the authentication backend. There are no auth controllers in this starter kit. Fortify registers the routes, and this app supplies the React screens and the actions behind them. Four of Fortify's features are enabled in config/fortify.php: registration, password reset, profile information updates and password updates. Everything else it ships (email verification, two-factor authentication, passkeys) is deliberately switched off, and you enable one by adding it back to the features array.
That gives you /login, /register, /forgot-password, /reset-password/{token} and /user/confirm-password, rendered by the Inertia pages in resources/js/Pages/Auth/, plus the PUT /user/profile-information and PUT /user/password endpoints that the Profile page submits to. The views are bound in app/Providers/FortifyServiceProvider.php and the actions behind each feature (create user, reset password, update profile information, update password) live in app/Actions/Fortify/. A successful login, registration or password confirmation lands on /dashboard.
Password reset needs real mail configuration in production. .env.example ships MAIL_MAILER=log, so reset links are written to storage/logs/laravel.log locally instead of being delivered.
Intervention Image
Intervention Image does the server-side avatar resizing in app/Actions/Profile/StoreAvatar.php (see Profile Page).
Laravel Person Name
A validation rule (ValidPersonName) for first and last names, applied by the Fortify create-user and update-profile actions.
Hashids
A Hashids bridge for Laravel, included for obfuscating sequential ids in URLs. Nothing in the starter kit uses it yet; it is installed and configured so it is ready when you need it.
Frontend
React
React 19 with TypeScript. Entry point is resources/js/app.tsx with SSR support via resources/js/ssr.tsx.
Shark UI
Components come from Shark UI: shadcn/ui-style components built on
Ark UI and styled with Tailwind CSS and tailwind-variants. Like shadcn/ui, the source is
copied into resources/js/components/ui/ and is yours to edit. Behavior and accessibility (keyboard handling,
focus management, ARIA) come from Ark UI.
components.json registers the @shark registry, so new components install with the shadcn CLI:
npx shadcn@latest add @shark/<component>
Dependencies: @ark-ui/react, tailwind-variants, tailwind-merge, clsx and Lucide icons.
A few things differ from shadcn/ui and Radix:
- Ark APIs.
Selecttakes a collection built withcreateListCollectionand astring[]value. Change handlers receive a details object:onValueChange={({ value }) => …},onCheckedChange={({ checked }) => …},onOpenChange={({ open }) => …}. - Sizes.
Buttonsizes arexs/sm/md/lg/xlplusicon-xs…icon-xl;mdis the default andlgmatches shadcn's default height.Buttonalso takesisLoading, which swaps the label for a spinner. - Fields. Wrap a control in
FieldwithFieldLabelandFieldErrorso Ark wires up the label,aria-*attributes and invalid styling.components/form-field.tsxdoes this for text inputs. - Status colors.
Badge,ButtonandAlertuse thesuccess,infoandwarningtokens inresources/css/app.css.--destructive-foregroundis the red text tone for tinted surfaces, not white.
Local fix: SelectGroup in components/ui/select.tsx rendered its heading only when none was passed. It has
been corrected in this copy, so re-check it if you re-install select from the registry.
Tailwind CSS
Tailwind CSS v4 with the Vite plugin. Brand palette, semantic color tokens and dark mode support live in resources/css/app.css.
Toast Notifications
goey-toast powers the flash-message toaster (resources/js/components/flash-toaster.tsx). It declares
framer-motion as a non-optional peer dependency (floor >=10.0.0), so framer-motion is installed either
way; it is listed directly in package.json at ^13.2.0 so the resolved version is one we choose rather
than one that drifts to goey-toast's much older floor. Nothing in resources/js imports framer-motion
directly, so do not remove it thinking it is unused.
Debugging
Spatie Laravel Ray
Spatie Laravel Ray is included to help you debug your Laravel applications with ease.
Testing
Pest
Pest is included to help you write expressive and elegant tests for your Laravel applications.
Spatie Pest Expectations
Pest Plugin - Faker
Pest Plugin - Laravel
Pest Plugin - Type Coverage
Pest Plugin - Browser
Browser testing through Playwright. tests/Pest.php already binds the Browser directory to the Laravel test case, so create tests/Browser/ and start writing visit() tests. playwright is in devDependencies; run npx playwright install once before the first browser test.
Laravel Pao
Pao reformats Pest and PHPStan output into a compact form that is easier for AI coding agents to read.
Larastan
Larastan is included to help you catch type errors in your Laravel applications using PHPStan.
Pint
Pint is included to help you format your Laravel code according to the Webminty coding standard.
Rector Laravel
Rector, with the Laravel rule set, is included to help you refactor and upgrade your codebase automatically.
Formatting
Prettier
Prettier is included to help you format your code consistently across your project.
Prettier Plugin - Tailwind CSS
Prettier Plugin - Blade
AI Tooling
Laravel Boost Guidelines
Webminty's Laravel and PHP coding guidelines, packaged for Laravel Boost so AI coding agents working in this repository follow the same conventions the human contributors do.
Development
composer run dev starts the PHP server, queue listener, log tail and Vite together. composer run dev:ssr does the same with the Inertia SSR server instead of the Vite dev server (it builds the SSR bundle first).
| Task | Command |
|---|---|
| Tests | vendor/bin/pest |
| Static analysis | vendor/bin/phpstan |
| PHP formatting | vendor/bin/pint |
| Rector | vendor/bin/rector |
| TypeScript check | npm run typecheck |
| Prettier | npm run format (or format:check) |
| Production build | npm run build (add :ssr for the SSR bundle) |
Application Shell
The authenticated app (sidebar, top bar, and pages like Settings) ships as a starting shell, not a finished product.
- Navigation is a single list edited in
resources/js/components/app-shell/navigation.ts. Add, remove, or reorder entries there rather than hunting through the sidebar and mobile drawer components separately. - Appearance (light/dark/system) lives on the authenticated user's record, so it follows them across devices. It's also mirrored into an
appearancecookie on save, purely so the very first server-rendered response can paint the right theme before Inertia props are available - without that mirror, the page would flash the wrong theme on load. The cookie ishttpOnlyand read only on the server; no JavaScript touches it. It's exempt from Laravel's cookie encryption inbootstrap/app.php, sharing that exemption list with thesidebar_collapsedcookie below, which JavaScript genuinely does read and write. - Sidebar collapse is stored in a cookie rather than
localStorage. This starter has SSR wired up, andlocalStorageisn't available during a server render - a cookie is, so the collapsed/expanded state can be read on the very first render instead of flashing open and then collapsing. - Originality is still your job. This shell (layout, components, and defaults) is intentionally generic so it can serve any product. A real product built on this starter still needs its own product promise and a signature visual device that makes it feel like something, not a starter kit.
Profile Page
/profile (resources/js/Pages/Profile.tsx) gives the signed-in user five independent forms, each backed by its own controller and validated into its own named Laravel error bag: profile information (name/email) and password (both submitted to Fortify's own endpoints), avatar, browser sessions, and account deletion. Each form's useForm() call passes its bag name only as the errorBag request option on that form's submit call, so a validation error from one form never renders under another form's field - this only works because every submit call names its bag explicitly. Never pass that bag name (or anything else) as a leading string argument to useForm() itself: in @inertiajs/react that argument is a history remember key, which mirrors the form's state into window.history.replaceState on every keystroke, and three of these forms hold a plaintext password.
Avatars are resized to a 256px square WebP (app/Actions/Profile/StoreAvatar.php) and stored on the public disk. That disk is only browser-reachable through the storage symlink, which php artisan storage:link creates - already wired into composer.json's post-create-project-cmd, so a fresh laravel new --using=... install has it from the start. If avatars 404 in an existing checkout, run php artisan storage:link yourself.
The browser sessions list and "Log out other devices" both depend on SESSION_DRIVER=database - they read from and delete rows in the sessions table. With any other driver the list renders empty and the logout button evicts nobody, though it still reports success.
Two features Fortify ships are still switched off and are not part of this page: email verification and two-factor authentication. Both are cycle 2b work - enabling them means adding a "Verify email" prompt and a two-factor section here.
Demo Pages
Two of this starter's routes render a worked example rather than a blank page, so a cloned application has real layouts, states, and content density to hold itself to instead of starting from nothing. Both are fiction - fixed sample data, not features meant to ship - and both are built to be deleted together.
/dashboard- a customer-onboarding pipeline: metrics, a chart, an attention queue and a table. The page shape a dashboard needs./vehicles- a fleet of 45 vehicles, listed, viewed and edited. The page shape almost every dashboard application actually needs next: a list with real server-side search, filtering, sorting and pagination; a detail view; and a form that validates and saves.
The two use deliberately unrelated vocabularies. That is the honest signal that both are disposable examples rather than two halves of a half-built product.
States
Every dashboard panel reads its state from a ?state= query switch:
populated(default) - every panel ready with sample data.empty- every panel in its empty, first-run state.loading- every panel showing its skeleton.partial- the chart panel is unavailable while everything else stays ready. Demonstrates that one stalled panel shouldn't block the rest of the page.error- everything ready except the chart, which shows a load failure and a retry action.
An unrecognised value falls back to the populated state rather than erroring.
The vehicle list carries the same switch, with the two states a list has: ?state=empty for a fleet with
nothing in it yet, and ?state=loading for the skeleton. The detail and edit pages have none - they always
have a record, so an empty state there would be invented rather than demonstrated. Filtering to nothing is a
third, different state, and the list says so in different words with a way to undo it: try /vehicles?q=zzzz.
An unrecognised value here falls back to ready rather than erroring - the vehicle list's own default state,
not the dashboard's populated.
Data
The dashboard's numbers come from a deterministic PHP provider with no database behind it. The vehicles are a
real table, demo_vehicles, because an edit form that appears to save but changes nothing teaches the wrong
thing, and search, sorting and pagination are only worth copying if they are the real server-side
implementations.
That table is created and populated by a migration inside app/Demo/, loaded by
App\Demo\DemoServiceProvider. A migration that seeds is unusual on purpose: it means a fresh clone is
populated by php artisan migrate alone, every test starts from the same 45 rows, and removing the demo never
has to touch database/seeders/DatabaseSeeder.php. The cost is that those rows exist in every test's database,
including tests that have nothing to do with vehicles.
Removing both
When you are ready to start building your own application, run the removal script from the project root:
./remove-demo.sh
It asks for confirmation (pass --yes to skip the prompt), then performs the entire contract below for you:
it deletes the three demo directories, restores the /dashboard route to DashboardController, unregisters
the demo service provider, removes the Vehicles navigation entry, drops the demo_vehicles table, and clears
the cached config and routes. When it finishes it deletes itself - a starter kit with no demo has no use
for a demo-removal script, so remove-demo.sh is not something you carry into your own project. The one thing
it deliberately leaves behind is chart.js in package.json; remove that yourself if you want it gone.
If you would rather do it by hand - or want to understand exactly what the script does - the contract is below.
rm -rf app/Demo resources/js/Pages/Demo tests/Feature/Demo
chart.js is a dependency only the demo uses (via resources/js/Pages/Demo/chart.tsx); it can be uninstalled
in the same pass with npm uninstall chart.js.
Then three one-line edits:
-
In
bootstrap/providers.php, delete theApp\Demo\DemoServiceProvider::classentry. -
In
routes/web.php, point thedashboardroute back at the fallback controller that already sits in the tree unrouted for exactly this, restore its import, and delete the fourvehicles.*routes:Route::get('/dashboard', DashboardController::class)->name('dashboard');
The route's
name('dashboard')does not change, so the Dashboard entry inresources/js/components/app-shell/navigation.tsneeds no edit - it links by route name, not by component. -
In
resources/js/components/app-shell/navigation.ts, delete theVehiclesentry and the now-unusedCaricon import.
One thing the contract cannot do for you: deleting the migration does not drop demo_vehicles from a database
that has already run it. A fresh clone never creates the table. An existing install drops it by hand. The
deleted migration's row in Laravel's own migrations tracking table is left orphaned too, but that is benign -
modern Laravel prints "Migration not found" for it and continues rather than failing.
A test suite enforces the rest as the starter grows. The isolation test under the demo's own test directory
fails the build the moment anything outside those three directories references the demo namespace, so a stray
import can't quietly widen the removal surface. routes/web.php and bootstrap/providers.php are its only two
exceptions, both named explicitly. Laravel's generated provider manifest under bootstrap/cache/ is also
exempt from the scan, because it is build output that regenerates itself and is never committed - a reader who
greps for the demo's name and finds a hit in bootstrap/cache/services.php should not conclude the removal
contract is broken. The navigation entry is the one piece no scan can catch - it links by route name and
contains nothing that looks like the demo - so it has a test of its own that disappears along with the demo.
This contract was executed for real - directories deleted, provider and routes and navigation stripped, full suite and production build run - as part of building this work, to prove it rather than just assert it.
What is not part of the demo
badge, field, input, select, switch and textarea in components/ui/ are general-purpose primitives the rest of the
application uses too, and are not part of the removal. table and skeleton are the same kind of stock
primitive, but the demo currently happens to be their only consumer - they stay for the same reason every
other unused primitive does: an application built on this starter will want a table and a loading skeleton,
and re-adding them by hand is worse than leaving them. Neither is components/form-field.tsx, which the
profile and auth pages use.
The demo's own components live under its page directory rather than the shared components/ tree - a
deliberate deviation - specifically so that deleting the demo never means picking components back out of a
shared folder.
What the tests don't cover
There are no browser tests in this repository, and the vehicle list has two behaviours that only a browser
exercises: the debounce on the search box (the server-side result of a search is tested; the fact that typing
issues one request rather than one per keystroke is not), and the click behaviour of a sortable column header
(the sorted result is tested; the header's own state is not). ?state=loading proves the skeleton branch
renders - not that it appears at the right moment during a real in-flight visit.
Additional Configurations
Changes to the default Laravel files are included in this starter kit to improve performance and developer experience.
app/Providers/AppServiceProvider.php
Model::preventLazyLoading(); if ($this->app->isProduction()) { Model::handleLazyLoadingViolationUsing(function ($model, $relation) { $class = get_class($model); info("Attempted to lazy load [{$relation}] on model [{$class}]."); }); DB::prohibitDestructiveCommands(); } else { Model::preventAccessingMissingAttributes(); Model::preventSilentlyDiscardingAttributes(); Model::shouldBeStrict(); } Vite::usePrefetchStrategy('aggressive');
Thank you Webminty Team