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
- Use TypeScript: Type safety prevents bugs and improves DX
- Export via index.ts: Make public APIs explicit
- Use ModulePage wrapper: Consistent layout and module checks
- Handle loading states: Show skeletons during data fetching
- Handle errors gracefully: Display user-friendly error messages
- Use shadcn/ui: Consistent, accessible UI components
- Colocate code: Keep related code together in the module
- 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
- Backend Development - Connect to backend APIs
- Module Structure - Understand file organization
- Creating Modules - Quick start guide