Skip to content

YeboCars Frontend

React application architecture with TikTok-style car browsing and dealer dashboard.

Technology Stack

TechnologyPurpose
ReactUI framework
TypeScriptType safety
React RouterNavigation
TanStack QueryServer state management
ZustandClient state
Tailwind CSSStyling
Framer MotionAnimations

Project Structure

src/
├── components/
│   ├── TikTokBrowser/       # Main car browser
│   │   ├── cards/           # Full-screen car cards
│   │   ├── media/           # Media player components
│   │   ├── modals/          # Search, filters, contact
│   │   ├── slider/          # Car slider logic
│   │   ├── ui/              # Car info, actions
│   │   └── desktop/         # Desktop layout
│   ├── auth/                # Login, signup, OTP
│   ├── dealer/              # Dealer dashboard
│   ├── search/              # Search components
│   └── common/              # Shared components
├── pages/                   # Route pages
├── hooks/                   # Custom React hooks
├── services/                # API clients
├── stores/                  # Zustand stores
├── types/                   # TypeScript types
└── utils/                   # Helper functions

Core Components

TikTok-Style Car Browser

Full-screen vertical swipe interface for car discovery:

tsx
// components/TikTokBrowser/cards/FullScreenCarCard.tsx
export function FullScreenCarCard({ car, onSwipe, onInquiry }) {
  return (
    <div className="h-screen w-full relative snap-start">
      {/* Full-screen media */}
      <MediaPlayer 
        media={car.images} 
        videos={car.videos}
        className="absolute inset-0 object-cover"
      />
      
      {/* Gradient overlay */}
      <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
      
      {/* Car info (bottom left) */}
      <CarInfo 
        make={car.make}
        model={car.model}
        year={car.year}
        price={car.formattedPrice}
        mileage={car.mileage}
        seller={car.sellerName}
        className="absolute bottom-20 left-4"
      />
      
      {/* Action buttons (bottom right) */}
      <CarActions
        carId={car.id}
        favorites={car.favorites}
        onInquiry={onInquiry}
        onFavorite={() => handleFavorite(car.id)}
        onShare={() => handleShare(car)}
        className="absolute bottom-20 right-4"
      />
      
      {/* Quick specs badge */}
      <div className="absolute top-4 left-4">
        <SpecsBadge 
          fuelType={car.fuelType}
          transmission={car.transmission}
          bodyType={car.bodyType}
        />
      </div>
    </div>
  );
}

Car Info Component

tsx
// components/TikTokBrowser/ui/CarInfo.tsx
export function CarInfo({ make, model, year, price, mileage, seller, className }) {
  return (
    <div className={cn("text-white", className)}>
      {/* Title */}
      <h1 className="text-2xl font-bold">
        {year} {make} {model}
      </h1>
      
      {/* Price */}
      <p className="text-3xl font-bold text-orange-400 mt-1">
        {price}
      </p>
      
      {/* Mileage */}
      <p className="text-sm text-gray-300 mt-1">
        {mileage.toLocaleString()} km
      </p>
      
      {/* Seller */}
      <div className="flex items-center gap-2 mt-3">
        <Avatar src={seller.avatar} size="sm" />
        <div>
          <p className="text-sm font-medium">{seller.name}</p>
          {seller.isDealer && (
            <Badge variant="dealer" size="xs">Dealer</Badge>
          )}
        </div>
      </div>
    </div>
  );
}

Car Actions Component

tsx
// components/TikTokBrowser/ui/CarActions.tsx
export function CarActions({ carId, favorites, onInquiry, onFavorite, onShare }) {
  const [isFavorited, setIsFavorited] = useState(false);
  
  return (
    <div className="flex flex-col gap-4 items-center">
      {/* Favorite */}
      <ActionButton
        icon={isFavorited ? HeartFilledIcon : HeartIcon}
        count={favorites}
        active={isFavorited}
        onClick={() => {
          setIsFavorited(!isFavorited);
          onFavorite();
        }}
      />
      
      {/* Inquiry */}
      <ActionButton
        icon={MessageIcon}
        label="Inquire"
        onClick={onInquiry}
      />
      
      {/* Test Drive */}
      <ActionButton
        icon={CarIcon}
        label="Test Drive"
        onClick={() => openTestDriveModal(carId)}
      />
      
      {/* Share */}
      <ActionButton
        icon={ShareIcon}
        onClick={onShare}
      />
    </div>
  );
}

Search Components

AI Search Modal

tsx
// components/search/AISearchModal.tsx
export function AISearchModal({ onClose, onResults }) {
  const [query, setQuery] = useState('');
  
  const aiSearch = useMutation({
    mutationFn: (q: string) => api.ai.search(q),
    onSuccess: (data) => {
      onResults(data.availableCars);
      onClose();
    }
  });
  
  const suggestions = [
    "Reliable family SUV under $30,000",
    "Electric car with good range",
    "Affordable first car for a student",
    "Luxury sedan for business"
  ];
  
  return (
    <FullScreenModal onClose={onClose}>
      <div className="p-4">
        <h2 className="text-xl font-bold mb-4">Find Your Perfect Car</h2>
        
        {/* Search input */}
        <div className="relative">
          <input
            type="text"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Describe what you're looking for..."
            className="w-full p-4 pr-12 border rounded-xl text-lg"
            autoFocus
          />
          <button
            onClick={() => aiSearch.mutate(query)}
            disabled={query.length < 3}
            className="absolute right-2 top-2 p-2 bg-orange-500 rounded-lg"
          >
            <SearchIcon className="w-6 h-6 text-white" />
          </button>
        </div>
        
        {/* Suggestions */}
        <div className="mt-6">
          <p className="text-sm text-gray-500 mb-3">Try searching for:</p>
          <div className="flex flex-wrap gap-2">
            {suggestions.map((suggestion) => (
              <button
                key={suggestion}
                onClick={() => {
                  setQuery(suggestion);
                  aiSearch.mutate(suggestion);
                }}
                className="px-3 py-2 bg-gray-100 rounded-full text-sm"
              >
                {suggestion}
              </button>
            ))}
          </div>
        </div>
        
        {/* Search intent interpretation */}
        {aiSearch.data?.searchIntent && (
          <div className="mt-4 p-3 bg-blue-50 rounded-lg">
            <p className="text-sm text-blue-600">
              🤖 Looking for: {aiSearch.data.searchIntent.intent}
            </p>
            <p className="text-xs text-blue-400 mt-1">
              Confidence: {(aiSearch.data.searchIntent.confidence * 100).toFixed(0)}%
            </p>
          </div>
        )}
      </div>
    </FullScreenModal>
  );
}

Filter Panel

tsx
// components/search/FilterPanel.tsx
export function FilterPanel({ filters, onChange }) {
  return (
    <div className="p-4 space-y-6">
      {/* Make */}
      <div>
        <label className="block text-sm font-medium mb-2">Make</label>
        <Select
          value={filters.make}
          onChange={(v) => onChange({ ...filters, make: v })}
          options={makes}
          placeholder="Any make"
        />
      </div>
      
      {/* Price Range */}
      <div>
        <label className="block text-sm font-medium mb-2">Price Range</label>
        <div className="flex gap-2">
          <Input
            type="number"
            value={filters.minPrice}
            onChange={(e) => onChange({ ...filters, minPrice: e.target.value })}
            placeholder="Min"
          />
          <Input
            type="number"
            value={filters.maxPrice}
            onChange={(e) => onChange({ ...filters, maxPrice: e.target.value })}
            placeholder="Max"
          />
        </div>
      </div>
      
      {/* Year Range */}
      <div>
        <label className="block text-sm font-medium mb-2">Year</label>
        <RangeSlider
          min={1990}
          max={new Date().getFullYear()}
          value={[filters.minYear, filters.maxYear]}
          onChange={([min, max]) => onChange({ ...filters, minYear: min, maxYear: max })}
        />
      </div>
      
      {/* Fuel Type */}
      <div>
        <label className="block text-sm font-medium mb-2">Fuel Type</label>
        <ChipGroup
          options={['Petrol', 'Diesel', 'Electric', 'Hybrid']}
          value={filters.fuelType}
          onChange={(v) => onChange({ ...filters, fuelType: v })}
        />
      </div>
      
      {/* Transmission */}
      <div>
        <label className="block text-sm font-medium mb-2">Transmission</label>
        <ChipGroup
          options={['Automatic', 'Manual']}
          value={filters.transmission}
          onChange={(v) => onChange({ ...filters, transmission: v })}
        />
      </div>
      
      {/* Body Type */}
      <div>
        <label className="block text-sm font-medium mb-2">Body Type</label>
        <ChipGroup
          options={['Sedan', 'SUV', 'Hatchback', 'Truck', 'Coupe', 'Van']}
          value={filters.bodyType}
          onChange={(v) => onChange({ ...filters, bodyType: v })}
        />
      </div>
    </div>
  );
}

Dealer Dashboard

Dashboard Overview

tsx
// components/dealer/DealerDashboard.tsx
export function DealerDashboard() {
  const { data: stats } = useQuery({
    queryKey: ['dealer-stats'],
    queryFn: api.dealer.getStats
  });
  
  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-6">Dealer Dashboard</h1>
      
      {/* Stats cards */}
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
        <StatCard
          title="Active Listings"
          value={stats.totalListings}
          icon={CarIcon}
        />
        <StatCard
          title="Total Views"
          value={stats.totalViews}
          icon={EyeIcon}
          trend="+12%"
        />
        <StatCard
          title="Inquiries"
          value={stats.monthlyInquiries}
          icon={MessageIcon}
          trend="+5%"
        />
        <StatCard
          title="Test Drives"
          value={stats.monthlyTestDrives}
          icon={CalendarIcon}
        />
      </div>
      
      {/* Quick actions */}
      <div className="flex gap-4 mb-8">
        <Button onClick={() => navigate('/dealer/add-car')}>
          <PlusIcon /> Add New Listing
        </Button>
        <Button variant="outline" onClick={() => navigate('/dealer/inventory')}>
          Manage Inventory
        </Button>
      </div>
      
      {/* Recent leads */}
      <section>
        <h2 className="text-lg font-semibold mb-4">Recent Leads</h2>
        <LeadsList leads={stats.recentLeads} />
      </section>
    </div>
  );
}

Inventory Management

tsx
// components/dealer/InventoryManager.tsx
export function InventoryManager() {
  const { data: inventory } = useQuery({
    queryKey: ['dealer-inventory'],
    queryFn: api.dealer.getInventory
  });
  
  return (
    <div className="p-6">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Inventory</h1>
        <Button onClick={() => navigate('/dealer/add-car')}>
          Add New Car
        </Button>
      </div>
      
      {/* Filters */}
      <div className="flex gap-4 mb-6">
        <Select
          placeholder="Status"
          options={['All', 'Active', 'Pending', 'Sold']}
        />
        <Input placeholder="Search by make, model..." />
      </div>
      
      {/* Car list */}
      <div className="bg-white rounded-lg shadow">
        <table className="w-full">
          <thead className="bg-gray-50">
            <tr>
              <th className="p-4 text-left">Car</th>
              <th className="p-4 text-left">Price</th>
              <th className="p-4 text-left">Status</th>
              <th className="p-4 text-left">Views</th>
              <th className="p-4 text-left">Actions</th>
            </tr>
          </thead>
          <tbody>
            {inventory?.cars.map(car => (
              <tr key={car.id} className="border-t">
                <td className="p-4">
                  <div className="flex items-center gap-3">
                    <img src={car.images[0]} className="w-16 h-12 object-cover rounded" />
                    <div>
                      <p className="font-medium">{car.year} {car.make} {car.model}</p>
                      <p className="text-sm text-gray-500">{car.mileage.toLocaleString()} km</p>
                    </div>
                  </div>
                </td>
                <td className="p-4">{car.formattedPrice}</td>
                <td className="p-4">
                  <StatusBadge status={car.status} />
                </td>
                <td className="p-4">{car.views}</td>
                <td className="p-4">
                  <Dropdown>
                    <DropdownItem onClick={() => navigate(`/dealer/edit/${car.id}`)}>
                      Edit
                    </DropdownItem>
                    <DropdownItem onClick={() => handleMarkSold(car.id)}>
                      Mark as Sold
                    </DropdownItem>
                    <DropdownItem onClick={() => handleDelete(car.id)} variant="danger">
                      Delete
                    </DropdownItem>
                  </Dropdown>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

Add/Edit Car Form

tsx
// components/dealer/CarForm.tsx
export function CarForm({ car, onSubmit }) {
  const { register, handleSubmit, watch } = useForm({
    defaultValues: car || {}
  });
  
  const vinLookup = useMutation({
    mutationFn: (vin: string) => api.vin.lookup(vin),
    onSuccess: (data) => {
      // Auto-fill form with VIN data
      setValue('make', data.make);
      setValue('model', data.model);
      setValue('year', data.year);
      setValue('fuelType', data.fuelType);
      setValue('transmission', data.transmission);
      setValue('bodyType', data.bodyType);
    }
  });
  
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      {/* VIN Lookup */}
      <section>
        <h2 className="text-lg font-semibold mb-4">Quick Fill with VIN</h2>
        <div className="flex gap-2">
          <Input
            {...register('vin')}
            placeholder="Enter VIN to auto-fill details"
          />
          <Button
            type="button"
            onClick={() => vinLookup.mutate(watch('vin'))}
            loading={vinLookup.isPending}
          >
            Lookup
          </Button>
        </div>
      </section>
      
      {/* Basic Info */}
      <section>
        <h2 className="text-lg font-semibold mb-4">Basic Information</h2>
        <div className="grid grid-cols-2 gap-4">
          <Select label="Make" {...register('make')} options={makes} required />
          <Select label="Model" {...register('model')} options={models} required />
          <Input label="Year" type="number" {...register('year')} required />
          <Input label="Price" type="number" {...register('price')} required />
          <Input label="Mileage (km)" type="number" {...register('mileage')} required />
          <Select label="Condition" {...register('condition')} options={['New', 'Used', 'Certified']} />
        </div>
      </section>
      
      {/* Specifications */}
      <section>
        <h2 className="text-lg font-semibold mb-4">Specifications</h2>
        <div className="grid grid-cols-2 gap-4">
          <Select label="Fuel Type" {...register('fuelType')} options={fuelTypes} />
          <Select label="Transmission" {...register('transmission')} options={transmissions} />
          <Select label="Body Type" {...register('bodyType')} options={bodyTypes} />
          <Input label="Engine Size" {...register('engineSize')} placeholder="e.g., 2.0L" />
          <Input label="Exterior Color" {...register('exteriorColor')} />
          <Input label="Interior Color" {...register('interiorColor')} />
        </div>
      </section>
      
      {/* Features */}
      <section>
        <h2 className="text-lg font-semibold mb-4">Features</h2>
        <FeatureSelector
          value={watch('features') || []}
          onChange={(f) => setValue('features', f)}
        />
      </section>
      
      {/* Media */}
      <section>
        <h2 className="text-lg font-semibold mb-4">Photos & Videos</h2>
        <MediaUploader
          images={watch('images') || []}
          videos={watch('videos') || []}
          onChange={(media) => {
            setValue('images', media.images);
            setValue('videos', media.videos);
          }}
        />
      </section>
      
      {/* Description */}
      <section>
        <h2 className="text-lg font-semibold mb-4">Description</h2>
        <Textarea
          {...register('description')}
          rows={5}
          placeholder="Describe the car, its history, condition..."
        />
        <Button
          type="button"
          variant="outline"
          className="mt-2"
          onClick={() => generateDescription()}
        >
          Generate with AI
        </Button>
      </section>
      
      <div className="flex gap-4">
        <Button type="submit" className="flex-1">
          {car ? 'Update Listing' : 'Create Listing'}
        </Button>
        <Button type="button" variant="outline" onClick={() => navigate(-1)}>
          Cancel
        </Button>
      </div>
    </form>
  );
}

Subscription/Billing UI

tsx
// components/dealer/SubscriptionPage.tsx
export function SubscriptionPage() {
  const { data: plans } = useQuery({
    queryKey: ['plans'],
    queryFn: api.billing.getPlans
  });
  
  const { data: subscription } = useQuery({
    queryKey: ['subscription'],
    queryFn: api.billing.getSubscription
  });
  
  const checkout = useMutation({
    mutationFn: (planId: string) => api.billing.createCheckout({ planId }),
    onSuccess: (data) => {
      window.location.href = data.url;
    }
  });
  
  return (
    <div className="p-6 max-w-4xl mx-auto">
      <h1 className="text-2xl font-bold mb-2">Choose Your Plan</h1>
      <p className="text-gray-600 mb-8">
        Unlock more listings and premium features
      </p>
      
      {/* Current plan */}
      {subscription && (
        <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-8">
          <p className="text-green-800">
            Current plan: <strong>{subscription.plan}</strong>
            {subscription.planExpiry && (
              <span className="text-sm ml-2">
                (expires {formatDate(subscription.planExpiry)})
              </span>
            )}
          </p>
        </div>
      )}
      
      {/* Plans */}
      <div className="grid md:grid-cols-3 gap-6">
        {plans?.map(plan => (
          <PlanCard
            key={plan.id}
            name={plan.name}
            price={plan.displayFormatted}
            usdPrice={plan.usdFormatted}
            showUsdNote={plan.showUsdNote}
            features={plan.features}
            listingLimit={plan.listingLimit}
            isPopular={plan.id === 'DEALER'}
            isCurrent={subscription?.plan === plan.id}
            onSelect={() => checkout.mutate(plan.id)}
            loading={checkout.isPending}
          />
        ))}
      </div>
    </div>
  );
}

API Services

typescript
// services/api.ts
const api = {
  auth: {
    login: (data) => post('/auth/login', data),
    signup: (data) => post('/auth/signup', data)
  },
  
  cars: {
    getFeed: (params) => get('/cars/feed', params),
    getById: (id) => get(`/cars/${id}`),
    search: (params) => get('/cars/search', params),
    create: (data) => post('/cars', data),
    update: (id, data) => put(`/cars/${id}`, data),
    favorite: (id) => post(`/cars/${id}/favorite`),
    inquiry: (id, data) => post(`/cars/${id}/inquiry`, data)
  },
  
  ai: {
    search: (query) => post('/ai/search', { query }),
    recommendations: (profile) => post('/ai/recommendations', profile),
    pricingInsights: (carId) => get(`/ai/pricing-insights/${carId}`)
  },
  
  vin: {
    lookup: (vin) => get(`/vin/lookup/${vin}`)
  },
  
  dealer: {
    getStats: () => get('/dealers/me/stats'),
    getInventory: () => get('/dealers/me/inventory'),
    getLeads: () => get('/dealers/me/leads')
  },
  
  billing: {
    getPlans: () => get('/api/billing/plans'),
    createCheckout: (data) => post('/api/billing/checkout', data),
    getSubscription: () => get('/api/billing/subscription')
  }
};

One chat. Everything done.