Frontend Development

Learn how to build beautiful, functional UI components for your Auto Commerce modules using React and Next.js.

Module Frontend Structure

modules/YourModule/
└── frontend/
    ├── index.ts           # Public exports (components, hooks, types)
    ├── components/
    │   ├── YourComponent.tsx
    │   └── YourForm.tsx
    ├── pages/             # Module pages (loaded dynamically)
    │   ├── index.tsx      # /your-module
    │   ├── settings.tsx   # /your-module/settings
    │   └── detail.tsx     # /your-module/[id]
    ├── hooks/
    │   └── useYourModule.ts
    ├── types/
    │   └── index.ts
    └── lib/
        └── api.ts

Dynamic Module System

Auto Commerce uses a dynamic module system where modules define their own:

  • Navigation items - Menu entries in the sidebar
  • Pages - Routes handled by the module
  • Components - Reusable UI components
  • Hooks - Data fetching and state management

Defining Routes in module.json

{
  "name": "YourModule",
  "alias": "your-module",
  "frontend": {
    "routes": {
      "basePath": "/your-module",
      "catchAll": true,
      "pages": {
        "/": "index.tsx",
        "/settings": "settings.tsx",
        "/items": "items.tsx",
        "/items/[id]": "item-detail.tsx"
      }
    },
    "navigation": {
      "title": "Your Module",
      "href": "/your-module",
      "icon": "Box",
      "position": 10,
      "children": [
        { "title": "Overview", "href": "/your-module", "icon": "LayoutDashboard" },
        { "title": "Settings", "href": "/your-module/settings", "icon": "Settings" }
      ]
    },
    "exports": {
      "components": ["YourComponent", "YourForm"],
      "hooks": ["useYourModule", "useYourData"],
      "types": ["YourType", "YourSettings"]
    }
  }
}

Navigation Icons

Icons are resolved from Lucide Icons. Use the icon name as a string:

"icon": "Package"      // LucideIcons.Package
"icon": "Settings"     // LucideIcons.Settings
"icon": "Truck"        // LucideIcons.Truck
"icon": "MessageSquare" // LucideIcons.MessageSquare

Creating Module Pages

Using the ModulePage Wrapper

The ModulePage component handles layout, module enablement checks, and consistent styling:

// pages/index.tsx
'use client';

import { ModulePage } from '@/components/modules/module-page';
import { YourContent } from '../components/your-content';

export default function YourModuleIndexPage() {
  return (
    <ModulePage
      moduleAlias="your-module"
      title="Your Module"
      description="Overview of your module features"
      breadcrumbs={[
        { label: 'Your Module' }
      ]}
      actions={
        <Button>
          <Plus className="h-4 w-4 mr-2" />
          Add New
        </Button>
      }
    >
      <YourContent />
    </ModulePage>
  );
}

ModulePage Props

Prop Type Description
moduleAlias string Module alias for enablement check
title string Page title
description string? Optional page description
breadcrumbs array? Breadcrumb navigation
actions ReactNode? Header action buttons
withLayout boolean? Include EnterpriseLayout (default: true)

Module Guard

For conditional rendering based on module enablement:

import { ModuleGuard } from '@/components/modules/module-page';

function MyComponent() {
  return (
    <div>
      <h1>Always Visible</h1>

      <ModuleGuard moduleAlias="channel-whatsapp">
        <WhatsAppFeatures />
      </ModuleGuard>

      <ModuleGuard
        moduleAlias="shipping-delhivery"
        fallback={<p>Enable Delhivery to see shipping options</p>}
      >
        <DelhiveryShipping />
      </ModuleGuard>
    </div>
  );
}

Exporting Module APIs

index.ts - Public Exports

// modules/YourModule/frontend/index.ts

/**
 * YourModule - Frontend Exports
 *
 * Import from '@modules/YourModule/frontend' to use these exports.
 */

// Types
export * from './types';

// Hooks
export { useYourModule, useYourData } from './hooks/use-your-module';

// Components
export { YourComponent } from './components/your-component';
export { YourForm } from './components/your-form';

Using Module Exports

Other modules or the main app can import your exports:

// In any page or component
import { useYourModule, YourComponent } from '@modules/YourModule/frontend';

function MyPage() {
  const { data, loading } = useYourModule();

  return (
    <div>
      <YourComponent data={data} />
    </div>
  );
}

Creating Components

Basic Component

// components/ConnectionStatus.tsx
import { useEffect, useState } from 'react';
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';

interface ConnectionStatusProps {
  moduleAlias: string;
}

export function ConnectionStatus({ moduleAlias }: ConnectionStatusProps) {
  const [status, setStatus] = useState<{
    connected: boolean;
    lastSync?: string;
  } | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchStatus();
  }, [moduleAlias]);

  const fetchStatus = async () => {
    try {
      const response = await fetch(`/api/v1/integrations/${moduleAlias}/status`);
      const data = await response.json();
      setStatus(data);
    } catch (error) {
      console.error('Failed to fetch status:', error);
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <div className="flex items-center gap-2 text-muted-foreground">
        <Loader2 className="h-4 w-4 animate-spin" />
        <span>Checking connection...</span>
      </div>
    );
  }

  return (
    <div className="flex items-center gap-2">
      {status?.connected ? (
        <CheckCircle className="h-4 w-4 text-green-500" />
      ) : (
        <XCircle className="h-4 w-4 text-red-500" />
      )}
      <span className="text-sm">
        {status?.connected ? 'Connected' : 'Not Connected'}
      </span>
      {status?.lastSync && (
        <span className="text-xs text-muted-foreground">
          Last sync: {new Date(status.lastSync).toLocaleString()}
        </span>
      )}
    </div>
  );
}

Settings Form with shadcn/ui

// components/SettingsForm.tsx
'use client';

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useToast } from '@/hooks/use-toast';
import { Loader2 } from 'lucide-react';

const settingsSchema = z.object({
  apiUrl: z.string().url('Must be a valid URL'),
  apiKey: z.string().min(1, 'API Key is required'),
  apiSecret: z.string().min(1, 'API Secret is required'),
});

type SettingsFormData = z.infer<typeof settingsSchema>;

export function SettingsForm() {
  const [saving, setSaving] = useState(false);
  const { toast } = useToast();

  const { register, handleSubmit, formState: { errors } } = useForm<SettingsFormData>({
    resolver: zodResolver(settingsSchema),
  });

  const onSubmit = async (data: SettingsFormData) => {
    setSaving(true);
    try {
      const response = await fetch('/api/v1/integrations/yourmodule/settings', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (!response.ok) throw new Error('Failed to save');

      toast({
        title: 'Settings saved',
        description: 'Your settings have been updated successfully.',
      });
    } catch (error) {
      toast({
        title: 'Error',
        description: 'Failed to save settings. Please try again.',
        variant: 'destructive',
      });
    } finally {
      setSaving(false);
    }
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Module Settings</CardTitle>
        <CardDescription>Configure your module integration</CardDescription>
      </CardHeader>
      <CardContent>
        <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="apiUrl">API URL</Label>
            <Input
              id="apiUrl"
              {...register('apiUrl')}
              placeholder="https://api.example.com"
            />
            {errors.apiUrl && (
              <p className="text-sm text-destructive">{errors.apiUrl.message}</p>
            )}
          </div>

          <div className="space-y-2">
            <Label htmlFor="apiKey">API Key</Label>
            <Input
              id="apiKey"
              type="password"
              {...register('apiKey')}
            />
            {errors.apiKey && (
              <p className="text-sm text-destructive">{errors.apiKey.message}</p>
            )}
          </div>

          <div className="space-y-2">
            <Label htmlFor="apiSecret">API Secret</Label>
            <Input
              id="apiSecret"
              type="password"
              {...register('apiSecret')}
            />
            {errors.apiSecret && (
              <p className="text-sm text-destructive">{errors.apiSecret.message}</p>
            )}
          </div>

          <div className="flex justify-end">
            <Button type="submit" disabled={saving}>
              {saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
              {saving ? 'Saving...' : 'Save Settings'}
            </Button>
          </div>
        </form>
      </CardContent>
    </Card>
  );
}

Custom Hooks

API Hook Pattern

// hooks/use-your-module.ts
'use client';

import { useState, useCallback } from 'react';

const API_BASE = process.env.NEXT_PUBLIC_API_URL || '/api/v1';

export interface YourItem {
  id: string;
  name: string;
  status: 'active' | 'inactive';
  createdAt: string;
}

export interface YourModuleResponse {
  data: YourItem[];
  meta: {
    current_page: number;
    total_pages: number;
    total_items: number;
  };
}

export function useYourModule() {
  const [items, setItems] = useState<YourItem[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchItems = useCallback(async (filters?: Record<string, string>) => {
    setLoading(true);
    setError(null);

    try {
      const params = new URLSearchParams(filters);
      const response = await fetch(`${API_BASE}/your-module/items?${params}`, {
        credentials: 'include',
      });

      if (!response.ok) {
        throw new Error('Failed to fetch items');
      }

      const data: YourModuleResponse = await response.json();
      setItems(data.data);
      return data;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Unknown error';
      setError(message);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  const createItem = useCallback(async (payload: Partial<YourItem>) => {
    const response = await fetch(`${API_BASE}/your-module/items`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error('Failed to create item');
    }

    const data = await response.json();
    return data.data as YourItem;
  }, []);

  return {
    items,
    loading,
    error,
    fetchItems,
    createItem,
  };
}

TypeScript Types

Defining Types

// types/index.ts

export interface ModuleSettings {
  apiUrl: string;
  apiKey: string;
  isActive: boolean;
  syncInterval: number;
}

export interface SyncStatus {
  status: 'idle' | 'syncing' | 'success' | 'error';
  lastSync?: string;
  error?: string;
  itemsSynced?: number;
}

export interface PaginatedResponse<T> {
  data: T[];
  meta: {
    current_page: number;
    total_pages: number;
    total_items: number;
    per_page: number;
  };
}

// Re-export for convenience
export type { YourItem, YourModuleResponse } from '../hooks/use-your-module';

Using shadcn/ui Components

Auto Commerce includes shadcn/ui components. Import from @/components/ui/:

import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

Best Practices

  1. Use TypeScript: Type safety prevents bugs and improves DX
  2. Export via index.ts: Make public APIs explicit
  3. Use ModulePage wrapper: Consistent layout and module checks
  4. Handle loading states: Show skeletons during data fetching
  5. Handle errors gracefully: Display user-friendly error messages
  6. Use shadcn/ui: Consistent, accessible UI components
  7. Colocate code: Keep related code together in the module
  8. Follow naming conventions: Use kebab-case for files, PascalCase for components

File Naming Conventions

Type Convention Example
Components PascalCase OrderStatusBadge.tsx
Hooks camelCase with use prefix use-orders.ts
Types PascalCase types/order.ts
Pages kebab-case create-manual.tsx
Utils camelCase format-currency.ts

Next Steps