A modern full-stack van rental platform built with React Router 7, showcasing advanced web development techniques including server-side rendering, authentication, and responsive design.
- Features
- Tech Stack
- Project Structure
- Database
- Authentication
- URL State Management
- SEO & Routing
- React 19 Features
- Getting Started
- Environment Variables
- Scripts
- Styling
- Code Quality
- Deployment
- Contributing
- 🚀 Modern React Router 7 with server-side rendering and file-based routing
- 🔒 Authentication with better-auth (sign up, login, session management)
- ⚛️ React 19 & Compiler (Activity component, native meta elements, automatic optimizations, lazy loading)
- 🚌 Van Management (CRUD operations, van types, image handling, state management, SEO-friendly slug URLs)
- 🖼️ Image Optimization (WebP format, responsive images, quality compression, modern formats)
- 💸 Rental System (rent, return, and manage van rentals)
- ⭐ Review System (rate and review rentals with analytics)
- 📈 Host Dashboard (income tracking, bar charts, rental analytics)
- 💰 Financial Management (deposit/withdraw funds, transaction tracking)
- 🏷️ Van State System (NEW, IN_REPAIR, ON_SALE, AVAILABLE with discount pricing)
- 💲 Dynamic Pricing (discount system with strikethrough original prices)
- 🎨 Modern UI/UX with responsive design, custom Tailwind variants, and smooth animations
- 🧑💻 TypeScript throughout with strict type checking
- 🧪 ArkType for runtime schema validation and type-safe narrowing
- 🗄️ Optimized Database IDs with 25-character CUID v2 and VARCHAR(25) constraints
- 🎨 TailwindCSS 4 with modern CSS features
- 📦 Prisma ORM with Neon PostgreSQL and relation joins
- 🔧 Generic Components for reusability and maintainability
- 📊 Sortable Data Tables with reusable sorting components
- 📱 Responsive Design with mobile-first approach
- ⚡ Performance Optimized with lazy loading and code splitting
- 🔗 URL State Management with nuqs for type-safe search parameters
- 🌐 View Transitions for smooth navigation experiences
- 🎯 Middleware-Driven Headers (automatic header forwarding via React Router v7 middleware)
- 🔄 Shared Context Middleware for eliminating duplicate data fetching between loaders and actions
- React 19.2.0 with stable Activity component for prerendering
- React Router 7.9.4 (file-based routing, SSR, optional route parameters)
- TypeScript 5.9.3 with strict configuration
- TailwindCSS 4.1.14 with modern CSS features
- Radix UI for accessible components
- Lucide React 0.501.0 for icons
- Recharts 3.3.0 for data visualization (lazy-loaded)
- nuqs 2.7.2 for type-safe URL state management
- Node.js with React Router server
- Prisma 6.17.1 ORM with Neon PostgreSQL (Rust-free client)
- better-auth 1.3.27 for authentication
- ArkType 2.1.23 for schema validation and type narrowing
- CUID2 2.2.2 for unique identifiers (configured for 25-character IDs)
- @prisma/adapter-neon 6.17.1 for Neon database integration
- Vite 7.1.10 - Next-generation frontend tooling with optimized builds
- React Compiler 1.0 (stable) - Automatic memoization and performance optimization
- Biome 2.2.6 for linting and formatting with Ultracite integration
- Ultracite 5.6.4 - AI-friendly linting rules for maximum type safety and accessibility
- Husky 9.1.7 for Git hooks and pre-commit automation with lint-staged
- TypeScript 5.9.3 with native preview
- Bun for fast package management and runtime
- Vite 7.1.10 - Fast builds with native ES modules and optimized bundling
- React Compiler - Configured via
vite-plugin-babel
for optimal integration - Automatic optimizations - React Compiler handles memoization without manual
useMemo
/useCallback
- Enhanced performance - Faster builds and reduced memory usage
- Type-safe configuration - Full TypeScript support in Vite config
app/
├── components/ # Reusable UI components
│ ├── ui/ # Shadcn UI components (buttons, inputs, cards, etc.)
│ └── [common] # Generic components (forms, lists, sortable, etc.)
├── constants/ # App-wide constants and enums
├── db/ # Database layer
│ ├── rental/ # Rental-related queries and transactions
│ ├── review/ # Review analytics and queries
│ ├── user/ # User analytics and payments
│ └── van/ # Van CRUD operations and queries
├── features/
│ ├── host/
│ │ ├── components/ # Host-specific components (charts, income, reviews)
│ │ └── utils/ # Route determination helpers
│ ├── image/ # Image optimization utilities
│ ├── middleware/ # Auth middleware and contexts
│ ├── navigation/ # Navigation components and hooks
│ ├── pagination/ # Pagination utilities and components
│ └── vans/
│ ├── components/ # Van UI (VanCard, VanDetail, HostVanDetail*, etc.)
│ ├── constants/ # Van-related constants
│ └── utils/ # Van helpers (pricing, styling, display)
├── hooks/ # Custom React hooks
├── lib/ # Server-side utilities
│ ├── auth.server.ts # Better-auth configuration
│ ├── parsers.ts # nuqs search parameter parsers
│ ├── schemas.server.ts # ArkType validation schemas
│ └── search-params.server.ts # Server-side search param loaders
├── routes/ # Route modules (Activity-based single routes)
│ ├── api/ # API routes
│ ├── auth/ # Authentication routes (login, signup, signout)
│ ├── host/ # Host dashboard routes (consolidated with Activity)
│ │ └── rentals/ # Rental management routes
│ ├── layout/ # Layout components
│ └── public/ # Public routes
│ ├── vans.tsx # Van listing/detail (Activity-based single route)
│ ├── home.tsx # Home page
│ ├── about.tsx # About page
│ └── 404.tsx # Not found page
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── assets/ # Static assets (SVGs, images)
├── root.tsx # Root component
└── routes.ts # Route configuration
prisma/
├── models/ # Modular Prisma model definitions
│ ├── betterAuth/ # Authentication models (User, Session, Account, Verification)
│ └── van/ # Van-related models (Van, Rent, Review, UserInfo, Transaction)
├── seed-data/ # Modular seed data files
├── schema.prisma # Prisma schema entrypoint
└── seed.ts # Database seeding script
- Neon PostgreSQL with Prisma ORM (Rust-free client)
- Modular schema with organized model files in subdirectories
- Config via prisma.config.ts (schema folder + seed command)
- Main models:
User
,Session
,Account
,Verification
- Authentication systemVan
- Van listings with types (SIMPLE, LUXURY, RUGGED), states (NEW, IN_REPAIR, ON_SALE, AVAILABLE), and SEO-friendly slugs for human-readable URLsRent
- Rental records and history (links to transactions)Review
- User reviews and ratingsUserInfo
- Extended user profile informationTransaction
- Single source of truth for all financial data (deposits, withdrawals, rental payments) with optional rental references and descriptions for complete audit trail
- Advanced features:
- Rust-free Prisma Client with
queryCompiler
anddriverAdapters
(now GA) - Relation joins for optimized queries (preview feature)
- Optimized CUID2 with 25-character IDs and VARCHAR(25) constraints for better performance
- Proper indexing and constraints with explicit column lengths
- Modular seed data organization with separate files for each model
- Enhanced seed data with varied van names, descriptions, and state management
- Van state system with NEW (client-derived), IN_REPAIR, ON_SALE, AVAILABLE states
- Discount pricing for ON_SALE vans with random discount percentages
- Slug-based routing with unique, SEO-friendly URLs (e.g.,
/vans/modest-explorer
) - ArkType regex validation for slugs with built-in length constraints
- Native JavaScript database drivers for better edge/serverless compatibility
- Rust-free Prisma Client with
# Generate Prisma client (Rust-free with relationJoins)
bunx prisma generate
# Push schema to database
bunx prisma db push
# Seed with enhanced data
bunx prisma db seed
This project uses prisma.config.ts
for Prisma CLI configuration (GA in Prisma 6.x):
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma',
migrations: {
seed: 'tsx prisma/seed.ts',
},
});
Notes:
- The deprecated
package.json#prisma
block has been removed. - Environment variables load via
dotenv/config
inprisma.config.ts
.
This project uses the Rust-free Prisma Client with the following configuration:
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
previewFeatures = ["relationJoins"]
engineType = "client"
}
Key Benefits:
- No Rust binary dependencies - eliminates native binary requirements
- Smaller bundle sizes - ideal for serverless and edge deployments
- Native JavaScript drivers - uses
@prisma/adapter-neon
for connection pooling - Better edge compatibility - works seamlessly in Vercel Edge Runtime
- Simplified deployments - no need to handle platform-specific binaries
- better-auth for secure email/password authentication
- Session management with proper security headers
- Protected routes with automatic redirects
- ArkType validation for all auth forms with custom narrow() validators
- Server-side session handling in loaders
- Modular model organization for better maintainability
- Centralized auth types/config in
app/lib/auth.server.ts
- Custom CUID v2 generator for 25-character user IDs optimized for database storage
The application uses nuqs 2.7.2 for type-safe URL state management:
- Type-safe search parameters with shared parsers between server and client
- Server-side loaders with
createLoader
for efficient data fetching - Client-side state management with
useQueryStates
- Bidirectional cursor pagination with forward/backward navigation
- Automatic URL synchronization with proper type handling
- View transitions support for smooth navigation
// Shared parsers (app/lib/parsers.ts)
export const paginationParsers = {
cursor: parseAsString.withDefault(DEFAULT_CURSOR),
limit: parseAsNumberLiteral(LIMITS).withDefault(DEFAULT_LIMIT),
direction: parseAsStringEnum(DIRECTIONS).withDefault(DEFAULT_DIRECTION),
type: parseAsVanType,
};
// Server-side loaders (app/lib/searchParams.server.ts)
export const loadSearchParams = createLoader(paginationParsers);
// Client-side usage
const [{ cursor, limit, direction, type }, setSearchParams] =
useQueryStates(paginationParsers);
React Router 7's middleware system enables efficient data sharing between loaders and actions:
- Eliminates duplicate fetching - Data fetched once in middleware, shared between loader and action
- Type-safe context - Fully typed shared data with TypeScript
- Cleaner code - Loaders and actions focus on business logic, not data fetching
- Better performance - Reduces database queries and API calls
import { createContext } from 'react-router';
// Define typed context
type SharedData = {
rent: NonNullable<Awaited<ReturnType<typeof getRent>>>;
balance: number;
};
const sharedDataContext = createContext<SharedData>();
// Fetch data once in middleware
const fetchDataMiddleware: Route.MiddlewareFunction = async (
{ params, context },
next
) => {
const [rent, balance] = await Promise.all([
getRent(params.rentId),
getBalance(session.user.id),
]);
context.set(sharedDataContext, { rent, balance });
return next();
};
export const middleware = [authMiddleware, fetchDataMiddleware];
// Synchronous loader - just retrieves from context
export function loader({ context }: Route.LoaderArgs) {
return context.get(sharedDataContext);
}
// Action also uses same data
export async function action({ context }: Route.ActionArgs) {
const { rent, balance } = context.get(sharedDataContext);
// Use shared data for validation/business logic
}
Note: Loaders can be synchronous when only retrieving data from context (no await
needed).
The application uses human-readable slugs for van URLs instead of database IDs:
- SEO-friendly URLs -
/vans/modest-explorer
instead of/vans/cmgg0wp450001zrijvbpx2uo0
- User-friendly - Shareable, memorable URLs for better user experience
- Type-safe validation - ArkType schema with regex validation
- Automatic generation - Slugs auto-generated from van names using
getSlug()
utility - Unique constraint - Database-enforced uniqueness with indexed lookups
- Internal ID usage - Database operations still use CUIDs for security and referential integrity
// Slug schema with built-in regex validation (1-70 chars, no leading/trailing hyphens)
export const slugSchema = type("/^[a-z0-9](?:[a-z0-9-]{0,68}[a-z0-9])?$/");
// Database lookup by slug
export async function rentVan(
vanSlug: string,
renterId: string,
hostId: string
) {
const van = await prisma.van.findUnique({
where: { slug: vanSlug },
select: { id: true },
});
// ... use van.id for database operations
}
// Routes use slugs
route(":vanSlug", "./routes/vans/van.tsx");
- Public van detail:
/vans/modest-explorer
- Host van detail:
/host/vans/beach-bum
- Rent van:
/host/rentals/rent/the-cruiser
The application features a comprehensive van state management system with dynamic pricing:
- NEW - Client-derived state for vans created within the last 6 months
- IN_REPAIR - Vans currently under maintenance (not rentable)
- ON_SALE - Vans with discount pricing applied
- AVAILABLE - Standard rentable vans
- Discount System - ON_SALE vans can have 5-100% discounts
- Price Display - Original price with strikethrough, discounted price highlighted
- VanPrice Component - Reusable component for consistent pricing display
- Smart Badges - VanBadge component shows relevant state information
- Client-side Derivation - NEW state computed from createdAt timestamp
// Van state with optional discount
model Van {
state VanState? @default(AVAILABLE)
discount Int? @default(0) @db.SmallInt
// ... other fields
}
// Dynamic pricing component
<VanPrice van={{ price, discount, state }} />
- Flexible pricing - Easy to manage sales and promotions
- State consistency - Prevents renting of unavailable vans
- User experience - Clear visual indicators for van status
- Maintainable - Centralized pricing logic in reusable components
The application features a reusable sorting system with type-safe generic utilities:
- Generic sorting utility (
app/lib/genericSorting.server.ts
) for any Prisma model - Reusable Sortable component (
app/components/common/Sortable.tsx
) for consistent UI - Type-safe orderBy clauses with full TypeScript support
- URL state integration with nuqs for persistent sorting preferences
- Four sort options: newest, oldest, highest, lowest
// Generic sorting utility
export function createGenericOrderBy<T>(
sort: SortOption,
config: SortConfig<T>
): T {
// Returns type-safe Prisma orderBy clause
}
// Reusable component
<Sortable
title="Reviews"
itemCount={reviews.length}
/>
// Database integration
const orderBy = createGenericOrderBy(sort, {
dateField: 'createdAt',
valueField: 'rating'
});
- Reviews page: Sort by newest/oldest date or highest/lowest rating
- Income page: Sort by newest/oldest date or highest/lowest amount
- Extensible: Easy to add sorting to any new data table
The application leverages React 19's modern features for better performance and developer experience:
React 19's stable Activity component enables instant navigation by prerendering multiple views:
import { Activity } from "react";
export default function Vans({ params }) {
const isDetailPage = params.vanSlug !== undefined;
return (
<>
<Activity mode={isDetailPage ? "visible" : "hidden"}>
<VanDetail />
</Activity>
<Activity mode={isDetailPage ? "hidden" : "visible"}>
<VanList />
</Activity>
</>
);
}
Benefits: Zero perceived latency between views, state preservation (scroll position, filters), memory efficient with paused effects.
Meta tags are defined directly within components using React 19's built-in elements:
export default function Home() {
return (
<section>
<title>Home | Van Life</title>
<meta name="description" content="Welcome to Van Life..." />
{/* rest of component */}
</section>
);
}
This replaces the deprecated meta
export pattern and removes the need for the <Meta />
component in root.tsx
.
The application uses React Compiler 1.0 for automatic performance optimizations:
// React Compiler automatically optimizes components
export default function MyComponent({ items }) {
// No manual useMemo/useCallback needed
const filtered = items.filter((item) => item.active);
return <List items={filtered} />;
}
Benefits: Automatic memoization, reduced boilerplate, better performance without manual optimization.
Heavy components like charts are code-split using React.lazy()
and Suspense
:
const BarChart = lazy(() => import("./BarChart"));
<Suspense fallback={<Skeleton />}>
<BarChart data={chartData} />
</Suspense>;
- Better Performance - Instant navigation, smaller bundles, automatic optimizations
- Improved SEO - Proper meta tags, social sharing support
- Simpler Code - Native elements, automatic memoization, no manual optimization
- Enhanced UX - Smooth transitions, progressive enhancement
- Node.js 18+
- Neon PostgreSQL database
- Bun (recommended) or npm
# Clone the repository
git clone <repository-url>
cd van-life
# Install dependencies
bun install
# or
npm install
# Set up environment variables
cp .env.example .env
# Edit .env with your Neon database credentials
# Set up database
bunx prisma generate
bunx prisma db push
bunx prisma db seed
# Start development server
bun run dev
The app will be available at http://localhost:5173.
# Build
bun run build
# Serve the production build
bunx @react-router/serve
Create a .env
file in the root directory:
# Database (Neon PostgreSQL)
DATABASE_URL=postgresql://user:[email protected]/neondb
# Authentication
BETTER_AUTH_SECRET=your-secret-key-here
BETTER_AUTH_URL=http://localhost:5173
# Optional: Google OAuth (commented out in env.server.ts)
# GOOGLE_CLIENT_ID=your-google-client-id
# GOOGLE_CLIENT_SECRET=your-google-client-secret
bun run dev
– Start development server with HMRbun run build
– Build for productionbun run typecheck
– TypeScript checking and route type generationbun run lint
– Run Biome lintingbun run lint:fix
– Fix linting issues automaticallybun run format
– Check code formattingbun run format:fix
– Fix formatting issues automaticallybun run check
– Run all checks (lint + format)bun run check:fix
– Fix all issues automaticallybun run ci
– Run CI checks
bunx ultracite init
– Initialize Ultracite in your projectbunx ultracite fix
– Format and fix code automaticallybunx ultracite check
– Check for issues without fixing
This project uses Husky with lint-staged for automated pre-commit checks:
- Pre-commit hook runs automatically before each commit
- lint-staged runs Ultracite only on staged files for efficiency
- Automatic formatting with Ultracite on staged files
- Commit blocking if any checks fail
- TypeScript configuration for type-safe setup
The pre-commit hook ensures code quality by:
- Running
bun x ultracite fix
on staged files via lint-staged - Blocking the commit if any step fails
Configuration in lint-staged.config.ts
:
import type { Configuration } from 'lint-staged';
const config: Configuration = {
'*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}': ['bun x ultracite fix'],
};
export default config;
Note: TypeScript config files work seamlessly with Bun's first-class TypeScript support. For Node.js, requires version 22.6.0+ or the --experimental-strip-types
flag.
- TailwindCSS 4.1.14 with modern features (container queries, view transitions, scroll-driven animations, CSS containment)
- Responsive design with mobile-first approach and CSS Grid layouts
- Biome configuration for CSS at-rules support
- Component variants using
class-variance-authority
for consistent UI - Custom Tailwind variants for van states (
van-new
,van-sale
,van-repair
,van-available
) - Centralized styling utilities -
getVanStateStyles()
function provides consistent styling across all van components - Type-safe styling with TypeScript support throughout
- Utility-first approach with custom CSS utilities for specific needs
- CSS custom properties for dynamic theming and reusable values
- Pseudo-random heights using CSS trigonometric functions for skeleton loaders
- Biome 2.2.6 for linting and formatting with Ultracite integration
- Ultracite 5.6.4 - AI-friendly linting rules for maximum type safety and accessibility
- TypeScript 5.9.3 with strict configuration
- ArkType 2.1.23 for runtime validation with regex support for slug validation
- Consistent code style:
- Tab indentation
- Single quotes
- Sorted CSS classes
- Organized imports
- Type safety throughout the application
- Error handling with proper error boundaries
- nuqs for type-safe URL state management
- Prisma with proper type generation and optimized ID constraints
This project uses GitHub Actions for automated code scanning via CodeQL.
- Location:
.github/workflows/codeql.yml
- Triggered on: push and pull requests to
master
, plus a weekly schedule - Language matrix: JavaScript/TypeScript
- Purpose: statically analyze the codebase for security vulnerabilities and quality issues
- Implementation:
github/codeql-action
(init
andanalyze
) withbuild-mode: none
(no manual build required) - Permissions: writes security events; reads packages, actions, and contents as needed
This project uses Ultracite for enhanced code quality and AI-friendly development:
- Zero configuration required - Works out of the box with sensible defaults
- Subsecond performance - Lightning-fast linting and formatting
- Maximum type safety - Strict TypeScript rules and accessibility standards
- AI-friendly code generation - Optimized for modern AI development workflows
- Accessibility enforcement - Built-in a11y rules and best practices
- React/Next.js specific rules - Tailored for modern React development
- Ultracite integration via
"extends": ["ultracite"]
in biome.jsonc - CSS at-rules support for TailwindCSS 4 features
- Sorted CSS classes for consistency
- TypeScript strict mode enabled
- Import organization and sorting
- Custom rules for class sorting and organization
The application is configured for Vercel deployment with:
- Prisma client generation via
postinstall
script - Neon database integration with
@prisma/adapter-neon
- Edge runtime compatibility with proper WASM handling
- Environment variable configuration for production
- Rust-free Prisma Client for optimized serverless deployments
# Production build
bun run build
# Type checking
bun run typecheck
# Linting and formatting
bun run check
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Follow the coding style guide (see
biome.json
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
- Use Biome with Ultracite for formatting and linting
- Follow TypeScript best practices with Ultracite's strict rules
- Write meaningful commit messages
- Add tests for new features
- Use nuqs for URL state management
- Follow the established project structure
- Follow Ultracite's accessibility and code quality standards
- Pre-commit hooks automatically ensure code quality before commits
This project is for educational/portfolio purposes and demonstrates modern full-stack web development best practices.
Built with ❤️ using React Router 7, TypeScript, nuqs, and modern web technologies.