Skip to content

Frontend (Vue 3 + Inertia.js v3)

Stack Summary

  • Vue 3 (Composition API, <script setup lang="ts">)
  • Inertia.js v3 (@inertiajs/vue3, @inertiajs/vite)
  • TypeScript
  • Vite 8 build pipeline
  • Tailwind CSS v4 (with forms & typography plugins) + Preline component library
  • Pinia for state management
  • Vue I18n for localization
  • Sentry SDK for error & performance monitoring

Directory Conventions

resources/js/
  Pages/        # Page components mapped to routes (Auth/, Client/, Shop/, Profile/, Public/)
  Components/   # Reusable presentational or functional components
  Elements/     # Domain UI building blocks (Agenda, TableComponent, Form, …)
  Directives/   # Custom Vue directives (clickoutside)
  Helper/       # Shared helper modules
  Hooks/        # Composables (useSearch, useModal, useTable, useShortcuts, …)
  Layouts/      # AppLayout, ClientLayout, GuestLayout (+ Partials)
  Locales/      # vue-i18n catalogs (en, fr)
  Modals/       # Modal components, resolved lazily by the modal plugin
  Plugins/      # auth, broadcasting, i18n, modal, switchDark, eventBus, …
  Stores/       # Pinia stores
  Utils/        # FormatNumber, FormatString, StringDate, UrlParams, HydrateEnums, …
  types/        # Domain TS definitions (generated.ts, enumRegistry.ts, Props.ts, forms.ts)
  app.ts        # Inertia app bootstrap
  • Do NOT edit types/generated.ts or types/enumRegistry.ts; both are regenerated from PHP.
  • Component tests live in __tests__/ folders next to the source.

Layout Resolution

Handled in app.ts:

  • Pages/Auth/* and the top-level Error page → GuestLayout
  • Pages/Client/*ClientLayout
  • everything else → AppLayout

Override with defineOptions({ layout: MyLayout }); opt out with noLayout: true.

Enum Hydration

Backend enums are serialized as { __enum, value } envelopes. hydrateEnums() (from @/Utils/HydrateEnums.ts) runs in the Inertia resolve hook on every page swap — initial load, visits, partial reloads and history restores — so props are already hydrated by the time a component renders. Read the hydrated value (or the helpers in @/types/enumRegistry.ts), never the raw envelope.

Routing & Navigation

  • Server-generated routes via tightenco/ziggy; always reference routes with the route() helper (or the useRoute composable).
  • Navigate with Inertia's <Link> rather than <a> to preserve SPA state. Import it as InertiaLink so it doesn't collide with the HTML <link> element.

HTTP & Forms

Two complementary APIs, both from @inertiajs/vue3:

  • useForm — classic Inertia form state (processing, errors, reset, visit callbacks). Still the default for page forms.
  • useHttp<TForm, TResponse>(data) — for requests that return data rather than a page visit. Use it instead of axios; the codebase has no axios dependency.

useHttp rules:

  • Bind http.<field> directly in templates — do NOT mirror fields into separate refs.
  • Type the payload with an explicit object type or Record<string, string | number | boolean | null | undefined> — not Record<string, unknown>.

Common Patterns

  • Disable the submit button while processing.
  • Show inline errors near fields.
  • Use preserveScroll to avoid scroll jumps on validation failure.

Gotchas

  • When mixing file + JSON data, rely on forceFormData to prevent ambiguous encoding.
  • Do not manually append CSRF tokens; Inertia handles this via Laravel middleware.

State Management (Pinia)

  • Stores live under Stores/.
  • Prefer explicit actions over directly mutating state for traceability.
  • Use stores for cross-page state only (ephemeral local state stays in components).

Internationalization

  • vue-i18n with catalogs in resources/js/Locales/{en,fr}, compiled by @intlify/unplugin-vue-i18n.
  • Server-side strings live in lang/; backend translatable model attributes use Spatie Translatable.
  • The active locale is shared through Inertia props and applied by setupI18n() in app.ts.

Styling (Tailwind v4)

  • Import Tailwind with @import "tailwindcss"; in the resources/css entry.
  • Use the semantic theme tokens (bg-primary, text-foreground, border-line-2, bg-card, …). They handle light and dark mode automatically — see theme.md for the full token reference. Do not hard-code hex values or raw palette colors.
  • Prefer gap-* over margin hacks for spacing in flex/grid containers.
  • dark: variants are for the rare override a token can't express; dark mode is toggled via @/Plugins/switchDark.ts.

Design System (Preline + Streamline Icons)

The UI builds on Tailwind v4 plus the Preline component library for accessible primitives. Preline is initialized in app.ts (HSStaticMethods.autoInit), with a MutationObserver re-initializing it after DOM updates — dynamically inserted Preline markup works without manual wiring.

Icons come from StreamlineHQ. Drop the SVG into resources/icons/; the Vite watcher runs npm run generate:icons and the icon becomes available through the generated barrel. Do not inline raw external SVGs in pages.

Components & Conventions

  • Single root element per Vue component.
  • Kebab-case component names in templates (ESLint vue/component-name-in-template-casing); PascalCase filenames.
  • One attribute per line (vue/max-attributes-per-line singleline: 1, plus Prettier's singleAttributePerLine).
  • 4-space indentation in both <template> and <script> (vueIndentScriptAndStyle).
  • Floating UI & tooltips via @headlessui-float/vue and vue-tippy (registered globally as the v-tooltip directive).

Realtime & Events

  • WebSockets via laravel-echo + pusher-js, connecting to the Reverb server; wiring lives in @/Plugins/broadcasting.ts.
  • Model broadcasts arrive on models.{morphClass}.{id} channels.

Error & Performance Monitoring

  • Sentry Vue integration initialized in app.ts (@sentry/vue with browser tracing and the Pinia plugin).
  • DSN comes from VITE_SENTRY_LARAVEL_DSN; source maps are uploaded by @sentry/vite-plugin at build time.

Testing (Vitest)

  • Unit & component testing via vitest + @testing-library/vue / @vue/test-utils.
  • DOM environment simulated using happy-dom (see vitest.config.ts and vitestSetupFile.js).
  • npm run test (watch), npm run test:coverage.

Imports & Aliases

  • Path alias @resources/js (defined in tsconfig.json and resolved by Vite).
  • Linting uses ESLint flat config (eslint.config.js) with eslint-plugin-import-x. The rule import-x/extensions is set to always with ignorePackages: true — relative imports need their .ts / .vue extension, bare package specifiers do not.

Performance Guidelines

  • Pages are code-split automatically by Inertia's dynamic resolvePageComponent imports; Pages/Auth/* is pinned to its own chunk and vendor code to a vendor chunk (see vite.config.js).
  • Lazy-load heavy modules (e.g., TipTap) with dynamic import.
  • Avoid large reactive objects in global stores; prefer granular refs.
  • Use watchEffect sparingly; prefer computed for derived state.

Common Patterns

Example page component:

vue
<script setup lang="ts">
import { Link as InertiaLink } from '@inertiajs/vue3';
import { ref } from 'vue';

import CarCard from '@/Components/Car/CarCard.vue';

const filter = ref('');
</script>

<template>
    <div class="space-y-4">
        <div class="flex items-center gap-2">
            <input
                v-model="filter"
                type="text"
                class="bg-layer border-line-2 rounded-lg border px-3 py-2"
                placeholder="Search"
            />
            <inertia-link
                :href="route('shop.car.create')"
                class="bg-primary text-primary-foreground hover:bg-primary-hover rounded-lg px-4 py-2"
            >
                New car
            </inertia-link>
        </div>
        <car-card
            v-for="car in cars"
            :key="car.id"
            :car="car"
        />
    </div>
</template>

Proceed to dependencies.md for the package landscape.