Angular 22 frontend for the Roadly vehicle rental platform. It consumes the sibling vehicle-rental-system Express and PostgreSQL API.
Roadly provides a persistent global display-currency preference for USD, MYR, EUR, GBP, SGD, and AUD. Rental prices retain USD as their source currency, while ride, driver earning, and payout prices retain MYR. The UI uses backend-provided indicative conversion rates; Stripe checkout and financial records continue to use the original transaction currency.
Bookings and rides capture the selected display currency and exchange rate when the customer confirms. Historical cards and receipts use that stored snapshot and can show the original USD/MYR settlement amount, so later rate or preference changes do not rewrite a customer's confirmation.
- Responsive Roadly landing page
- Brand-aware car, bike, van, and SUV presentation
- Searchable vehicle catalogue with date-aware availability, sorting, type filters, and active-filter reset
- Vehicle image URLs with type-appropriate fallbacks
- Responsive specification-rich cards, loading skeletons, vehicle details, and similar-ride discovery
- Customer registration and sign-in
- Google sign-in with Better Auth and HttpOnly cookie sessions
- Authentication restored from the API without storing tokens in browser storage
- Cookie-authenticated Socket.IO connections and immediate authorization changes after role updates
- Two-step phone OTP onboarding before customer booking creation
- Protected booking creation, history, and eligible cancellation
- Authenticated customer and driver support with rental/ride context, persistent conversations, FAQs, and automatic refresh
- Visual vehicle selection with live date-range availability and price quotes
- Two-month availability calendar with blocked booking dates
- Alternative vehicle suggestions and a booking confirmation receipt
- Pickup and return branches and times
- Insurance, daily add-ons, special requests, promo codes, taxes, and refundable deposits
- Optional Stripe Checkout with Resend email and Twilio SMS integration status
- Sticky application header and full Roadly footer
- Light, dark, and system themes persisted in local storage
- Runtime language switching with complete English and Bahasa Melayu feature catalogs
- Separate Roadly Rides experience for immediate Bike, Car, and XL trips in Kuala Lumpur
- Google map location picking, signed MYR fare quotes, promotional discounts, pre-authorized cards or driver-confirmed cash settlement, and cancellation fees
- Dedicated driver console with online status, browser GPS, navigation, and controlled trip progression
- Driver ride rejection with a typed reactive reason form, required context for “Other,” safe automatic reassignment, and duplicate-submit protection
- Automatic nearest-driver matching after card authorization, Socket.IO trip updates, automatic final-fare capture, and opt-in Web Push notifications
- Passenger reassignment notices and administrator rejection audit details showing the driver, reason, optional explanation, and timestamp
- Role-protected admin routes
- Sticky header and responsive admin sidebar
- Dashboard statistics
- Fleet creation, editing, deletion, filtering, availability, specifications, location, and rating management
- Booking details and return workflow
- User role changes and deletion controls
- AG Grid Community data tables
- Server-side search, filtering, and sorting
- Fifteen-record API batches
- Automatic record appending when the grid reaches its scroll boundary
- Manual First, Previous, direct page, Next, and Last navigation
- Custom collapsible left filter panel with active-filter count and Clear all
- Mobile off-canvas filter drawer
- Roadly confirmation dialogs and toast notifications instead of browser alerts
- Support inbox with customer context, booking context, filters, priority, status, replies, and pagination
- Automatic ride matching with manual dispatch fallback, driver onboarding, live operations, automatic waiting charges, and driver-entered tolls
- Operational dispatch tabs with live counts, compact expandable ride rows, attention filters, and direct server-side pagination
- Dedicated driver operations route so onboarding and availability do not lengthen the ride queue
- Angular 22 standalone components
- TypeScript
- Angular Router and route guards
- Angular Forms and HTTP client
- Transloco runtime internationalization
- AG Grid Community
- Spartan accessible primitives with Roadly-owned Tailwind styling
- Spartan input, textarea, field, native-select, select, and searchable combobox primitives
- Tailwind CSS 4 through Angular's PostCSS pipeline
- Lucide icons through ng-icons
- ngx-sonner notifications
- SCSS and Roadly design tokens
- Vitest through the Angular CLI
- Node.js 22 recommended
- npm
- The sibling backend running on port
5000
Install dependencies:
npm installStart the backend from vehicle-rental-system:
npm run devStart Roadly from this repository:
npm startOpen http://localhost:4200.
The root .postcssrc.json enables Tailwind CSS 4 during Angular style processing:
{
"plugins": {
"@tailwindcss/postcss": {}
}
}The inner {} is intentional: it enables the plugin with its default options. Keep this file while Tailwind utility classes or Spartan components styled with Tailwind are used.
Angular environment files define the API base URL:
| Configuration | File | API URL |
|---|---|---|
| Development | src/environments/environment.ts |
http://localhost:5000/api/v1 |
| Production | src/environments/environment.production.ts |
https://express-project-iota.vercel.app/api/v1 |
When deploying the frontend to another origin, set the backend FRONTEND_URL environment variable to that origin.
Authentication requests use withCredentials; the backend session cookie is HttpOnly and unavailable to Angular code. Deploy the frontend and API on HTTPS, configure BACKEND_URL, and register https://<api-host>/api/v1/auth/session/callback/google as the Google OAuth redirect URI.
At application startup, Roadly calls /api/v1/auth/me before protected route guards run. A signed-out startup probe is handled silently; genuine sign-in and API errors continue to use Roadly toast messages. Signing out revokes the server session and clears only in-memory user state—authentication data is never written to localStorage.
For local Google OAuth, register this exact redirect URI:
http://localhost:5000/api/v1/auth/session/callback/google
The theme preference may still use localStorage; this is visual preference data and is unrelated to authentication.
Set googleMapsBrowserKey in the appropriate Angular environment file to a browser key restricted to the Maps JavaScript API and the deployed frontend origins. Keep the separate GOOGLE_MAPS_SERVER_KEY only in the backend environment for Routes API calls.
| Route | Access | Description |
|---|---|---|
/ |
Public | Roadly landing page |
/vehicles |
Public | Search and browse vehicles |
/rides |
Customer | Immediate KL ride booking |
/driver |
Driver | Assigned trips, GPS, progression, and rejection |
/bookings |
Authenticated | Customer booking history |
/support |
Authenticated | Customer or driver support inbox |
/login |
Guest | Sign in |
/register |
Guest | Create an account |
/auth/callback |
Public | Complete Google sign-in |
/complete-profile |
Authenticated | Verify customer phone |
/admin/overview |
Admin | Operations overview |
/admin/vehicles |
Admin | Fleet management |
/admin/bookings |
Admin | Booking management |
/admin/users |
Admin | User management |
/admin/support |
Admin | Support operations inbox |
/admin/rides |
Admin | Operational ride dispatch queue |
/admin/drivers |
Admin | Driver onboarding and availability |
src/app/
components/ Route and feature components
admin/ Admin shell and administration sections
auth/ Login and registration
bookings/ Customer bookings
home/ Public landing page
support/ Authenticated support and live inbox
rides/ Immediate rides and Google map picker
driver/ Driver availability and active trips
vehicles/ Public fleet catalogue
core/ Auth, guards, HTTP, models, interceptors, and themes
i18n/ Transloco loader and persisted language preference
layout/ Site header and footer
shared/ Reusable grids, dialogs, filters, cells, and notifications
Components use separate .component.ts, .component.html, .component.scss, and .component.spec.ts files.
Application forms remain typed Angular Reactive Forms. Their visual and accessible controls use the generated Spartan Helm layer under src/app/shared/ui:
hlmInputfor text, email, password, date, time, number, URL, telephone, and search inputshlmTextareafor multiline messages, notes, descriptions, and support replieshlm-native-selectfor short fixed choices such as statuses, roles, vehicle types, payment methods, and filtershlm-comboboxfor searchable or higher-cardinality choices such as verified-user onboarding and available-driver assignment- Spartan Field primitives for new validation layouts, descriptions, and accessible error messages
Use the appropriate Spartan primitive for future controls instead of introducing another custom/native form-control pattern. Keep business state in typed reactive forms and keep Roadly colours in the shared CSS design tokens.
| Command | Description |
|---|---|
npm start |
Run the Angular development server |
npm test -- --watch=false |
Run the test suite once |
npm run i18n:check |
Verify that every locale has the same translation keys |
npm run build |
Create a production build |
npm run watch |
Build continuously using the development configuration |
Roadly supports light, dark, and system preferences. The selection is stored under roadly_theme, and AG Grid, dialogs, public pages, and admin sections use the same theme tokens.
Roadly loads translation catalogs at runtime with Transloco. Locale files are public static assets:
public/assets/i18n/
en/
common.json
vehicles.json
bookings.json
rides.json
support.json
driver.json
admin.json
ms/
common.json
vehicles.json
bookings.json
rides.json
support.json
driver.json
admin.json
English is the source and fallback locale. Bahasa Melayu is the second production-ready locale. The selected locale is stored under roadly_language; this is presentation preference data and contains no authentication information. Shared UI and the vehicle, booking, ride, support, driver, and administration feature areas use the same runtime locale. Dates, numbers, MYR ride values, and USD rental values are formatted using that locale.
To add another language:
- Copy the complete
en/directory to a new ISO-language directory such ases/orpt/. - Translate values in every feature catalog without changing keys or interpolation tokens such as
{{year}}. - Register the code and label in
LanguageService, then add it toavailableLangsinapp.config.tsand to the parity script. - Run
npm run i18n:checkto catch missing or unexpected keys.
Admin grids retrieve data from server-side paginated endpoints. Each request loads 15 records. Scrolling to the grid boundary appends the next batch, while the paginator can jump directly to another page. Search, filters, and sorting reset the result to page one and request fresh data from the API.
The filter sidebar is a custom Roadly component because AG Grid's native Filters Tool Panel is an Enterprise feature.
An assigned driver can reject a ride before arrival by selecting Not available, Too far away, or Other. The form uses Angular typed reactive forms and Spartan button primitives; Other requires an explanation. While the request is being processed, its controls are disabled to prevent duplicate submissions.
After a successful rejection:
- The rejecting driver’s active-trip panel is cleared.
Not availablechanges that driver to offline.- The passenger sees that the previous driver was unavailable and whether Roadly is still searching or has reassigned the ride.
- The administrator sees the rejecting driver, reason, details, and timestamp in the dispatch queue.
- Realtime updates keep passenger, administrator, and replacement-driver sessions synchronized.
The ride dispatch route defaults to Needs action and separates active, completed, cancelled, and complete ride history into server-filtered tabs. Each tab retains 15-row API pagination with First, Previous, direct page, Next, and Last controls. Tab changes, filters, and manual pagination show compact skeleton rows while loading; background realtime refreshes remain non-blocking. Compact rows keep assignment visible while secondary payment, rejection, waiting, toll, and completed-fare details open on demand.
Operational attention filters isolate rides awaiting card authorization, rides with driver rejections, and cash trips awaiting driver confirmation. Driver onboarding and roster availability are managed separately at /admin/drivers; assignment dropdowns on /admin/rides continue to use the same live driver data.
The sign-in page links to /forgot-password. Angular sends a generic recovery request through Better Auth, then accepts the single-use token at /reset-password. Both screens use standalone components, typed reactive forms, Spartan inputs/buttons, theme variables, and the existing Transloco catalogs. A successful reset returns to sign-in and shows confirmation; invalid or expired links can request a replacement without exposing whether an account exists.
For local testing, start the backend, submit the forgot-password form, and use the URL printed by the backend when Resend is not configured. Production must configure RESEND_API_KEY and EMAIL_FROM.
Run the test command before committing changes that affect templates, components, or shared grid behavior. Focused component tests can be selected with Angular CLI's --include option.
- Vehicle images are remote URLs rather than uploaded and managed assets
- Browser GPS requires user permission and HTTPS outside localhost
- Push notifications require backend VAPID keys and an explicit customer opt-in
- Automatic matching uses the latest persisted driver position within the configured KL search radius and excludes drivers who rejected the same ride; production dispatch can later add explicit offer/accept timeouts and traffic-aware pickup ETAs