تفضيلات ملفات تعريف الارتباط
نستخدم ملفات تعريف الارتباط للحفاظ على تشغيل الموقع وتحسين تجربتك.
اعرف المزيد في سياسة الخصوصية .
N .
English العربية
الرئيسية عنّي أعمالي المدونة تواصل معي
10

TRX Car Rental - A Bilingual Laravel Catalog with a Custom Vue Admin

TRX
Year

2021

Status

Archived

Tech Stack
Laravel Tailwind CSS PHP Vue Inertia Laravel Jetstream

معرض الصور

TRX Car Rental is a Dubai car rental company. They needed a public website in English and Arabic, and a back office where their own staff could manage the entire fleet and every page of marketing copy. The site is now archived and offline, so this is a look back at the build itself.

I'll be straight about where this sits in my timeline. I built TRX in early 2021. By then I had close to four years on Laravel, but Inertia and Vue 3 were new ground, and I was still working across Flutter and WordPress at the same time. The architecture is solid. Some of the edges are the edges of an engineer learning a new front end. I'm keeping both in the writeup, because the honest version is more useful than a polished one.

The whole thing is built around one idea: the client owns the content. Cars, brands, categories, every marketing page, reviews, team members, the site logo — all of it is editable from the admin panel, in both languages, without touching code.


Core Capabilities

The site is a bilingual catalog and inquiry platform, not a booking engine. No checkout, no payment, no reservation calendar. A visitor browses the fleet, opens a car, and contacts the company to rent. That was the right scope for the business.

  • A bilingual public site (English + Arabic with full RTL) covering the home, services, about, contact, fleet, and car detail pages

  • A car catalog with search, filters by brand and category, daily/monthly/annual pricing, four image slots per car, and a related-cars block

  • Direct rental inquiry through WhatsApp, phone call, or an inquiry form — the paths that market actually uses

  • A favorites system tied to verified user accounts, so saved cars persist beyond browser state

  • User accounts with 2FA, email verification, and Google/Facebook social login

  • A contact form protected by Google reCAPTCHA v3 with a server-side score check

  • A custom Vue 3 admin panel covering the entire fleet, page content, team, reviews, services, users, roles, and settings


Architecture

One Inertia app, two surfaces

No separate REST API, no separate admin SPA. The public front end and the dashboard live in the same Inertia + Vue 3 application on one Laravel back end. Public routes are open. The dashboard sits behind an es-admin prefix and a role:super-admin middleware from spatie/laravel-permission. Two roles seeded: super-admin for the dashboard, user for the front-end account area.

Bilingual from the routing layer down

Localization starts at the route group with mcamara/laravel-localization, not at the view layer. Every URL is locale-prefixed. Direction is resolved once and shared to the entire Vue front end through Inertia's shared props, so every component reads layout direction from one source. The language switcher redirects to the same page in the target locale instead of dumping the visitor on the home page.

Translatable content stored as JSON

Translatable fields are stored as JSON columns and managed with spatie/laravel-translatable. The one piece of code I'm still happy with from this project is a small custom trait wrapping Spatie's. By default, Inertia would hand Vue the full English-and-Arabic object for every translated field. I overrode toArray() to flatten each translatable attribute to the active locale before it crosses into the props. The front end just receives plain strings. The admin panel still reads and writes per-locale, so editors update English and Arabic independently.

Editable pages as keyed JSON documents

Marketing pages aren't templates with hardcoded text. A single pages table stores content keyed by page_name, lang, and a JSON content blob. The admin edits the About page in Arabic, the home page reads from the same documents to render its blocks. Repeatable sections (reasons, reviews, team members, services) are their own translatable models with their own images.

Media pipeline

Images run through spatie/laravel-medialibrary. Each car has four image slots, and uploads convert automatically to WebP — a sharpened 368×232 thumb for listings and a cover for detail views. Same mechanism for the logo, team photos, review avatars, and service images.

Filtering and pagination

The car listings use spatie/laravel-query-builder, so filters come straight from the query string and survive pagination. Pages return twelve at a time. Each row is shaped through ->through() into exactly the props the Vue page needs, instead of leaking the full model.


Admin Experience

The dashboard is a hand-built Vue 3 interface, not a generated one. Flash session messages surface to Vue as shared props for consistent save feedback across every page.

Fleet management

Create and edit cars with four images, daily/monthly/annual rates, seats, mileage, gearbox, fuel, production year, and an availability toggle. Bulk delete and a one-click availability switch.

Brands, categories, and content

Brands and categories are translatable and used as filter facets on the front end. About, Services, Contact, and the home-page blocks are edited per language. Reasons, reviews, and team members are managed as repeatable sections with their own images and social links.

Members and roles

List, search, create, and edit users with attached profiles (name, phone, address, birth date, gender). Assign roles individually or in bulk via syncRoles(). Search spans name and email through a custom AllowedFilter callback.


Accounts and contact flow

Authentication runs on Laravel Jetstream + Fortify: email/password, 2FA, and email verification. On top of that, social login through Socialite for Google and Facebook.

The front-end account area is deliberately small: a favorites list powered by maize-tech/laravel-markable, and a profile page. Favoriting requires a verified email. Changing your email re-triggers verification automatically.

The contact form is validated server-side and protected by Google reCAPTCHA v3 through a custom rule that calls Google's verify endpoint and rejects scores below 0.5. Submissions are persisted, and the Contact model fires an email to the company inbox on create.


Performance

The performance work here is light, and that's the right framing for the project. Two pieces stand on their own: WebP image conversions through Spatie Media Library, so the site doesn't ship original uploads; and server-driven pagination with query-string-preserved filters on the car listings, with ->through() shaping each row to the minimum props the Vue page actually needs.


What I'd build differently today

Honestly, I'd rebuild this one from the ground up. Different architecture, real test coverage from day one, a proper cache layer for the marketing pages, queued listeners instead of side effects in model hooks, stricter validation throughout. And I wouldn't reach for Vue on a site like this — the reactivity needs are minimal enough that Livewire + Filament would cover the entire build with less moving parts and a better admin experience for the client.

None of this sank the project. It ran in production for a real business in two languages. But it's the gap between "works" and "how I write Laravel now," and that gap is the point of showing it.


My Role

  • Built the Laravel application end to end as the sole developer

  • Built the bilingual routing layer and the language switcher that preserves the visitor's current page

  • Built the translatable content layer and the custom trait that flattens translations into Inertia props

  • Built the keyed-JSON pages model for per-language marketing-page editing

  • Built the entire Vue 3 admin panel on Inertia: fleet, brands, categories, content pages, team, reviews, services, users, roles, and settings

  • Wired in Jetstream/Fortify auth, Google + Facebook social login, reCAPTCHA-protected contact, and the favorites flow

  • Designed the media pipeline with per-collection WebP conversions


CTA

If you're looking at this case study, you're seeing an honest record of what I shipped four years ago, what I'd build differently today, and the line between the two. The same product thinking — let the client own every piece of content, in both languages, from one panel — runs through every Laravel system I build now. The execution has just gotten sharper.

تواصل على واتساب