Custom Components
Extend the UI when auto-generated interfaces aren't enough
When to Use Custom Components
While Supasheet auto-generates most UI, you may need custom components for:
- Complex workflows or multi-step processes
- Custom visualizations beyond standard charts
- Integration with third-party services
- Specialized input controls
- Custom business logic presentation
Creating Custom Pages
Basic Custom Page
// app/home/custom-feature/page.tsx
import { getSupabaseServerClient } from '@/lib/supabase/clients/server-client';
export default async function CustomFeaturePage() {
const supabase = await getSupabaseServerClient();
const { data: products } = await supabase
.from('products')
.select('*')
.limit(10);
return (
<div className="container mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Custom Feature</h1>
<div className="grid grid-cols-3 gap-4">
{products?.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}With Client Interactivity
'use client';
import { useState } from 'react';
import { getSupabaseBrowserClient } from '@/lib/supabase/clients/browser-client';
export function InteractiveComponent() {
const [data, setData] = useState([]);
const supabase = getSupabaseBrowserClient();
async function loadData() {
const { data } = await supabase
.from('orders')
.select('*')
.order('created_at', { ascending: false });
setData(data || []);
}
return (
<div>
<button onClick={loadData}>Load Data</button>
{/* Render data */}
</div>
);
}Custom Forms
Using React Hook Form
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { getSupabaseBrowserClient } from '@/lib/supabase/clients/browser-client';
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
quantity: z.number().min(1),
});
type FormData = z.infer<typeof schema>;
export function CustomOrderForm() {
const supabase = getSupabaseBrowserClient();
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
async function onSubmit(data: FormData) {
const { error } = await supabase
.from('orders')
.insert(data);
if (error) {
console.error(error);
return;
}
// Success!
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<input {...register('name')} placeholder="Name" />
{errors.name && <span className="text-red-500">{errors.name.message}</span>}
<input {...register('email')} placeholder="Email" />
{errors.email && <span className="text-red-500">{errors.email.message}</span>}
<input {...register('quantity', { valueAsNumber: true })} type="number" />
{errors.quantity && <span className="text-red-500">{errors.quantity.message}</span>}
<button type="submit">Submit</button>
</form>
);
}Multi-Step Form
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
type Step1Data = { name: string; email: string };
type Step2Data = { address: string; city: string };
type Step3Data = { payment: string };
export function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState<Partial<Step1Data & Step2Data & Step3Data>>({});
const { register, handleSubmit } = useForm();
function handleStep1(data: Step1Data) {
setFormData({ ...formData, ...data });
setStep(2);
}
function handleStep2(data: Step2Data) {
setFormData({ ...formData, ...data });
setStep(3);
}
async function handleStep3(data: Step3Data) {
const finalData = { ...formData, ...data };
// Submit to Supabase
}
return (
<div>
{step === 1 && (
<form onSubmit={handleSubmit(handleStep1)}>
<input {...register('name')} placeholder="Name" />
<input {...register('email')} placeholder="Email" />
<button type="submit">Next</button>
</form>
)}
{step === 2 && (
<form onSubmit={handleSubmit(handleStep2)}>
<input {...register('address')} placeholder="Address" />
<input {...register('city')} placeholder="City" />
<button type="button" onClick={() => setStep(1)}>Back</button>
<button type="submit">Next</button>
</form>
)}
{step === 3 && (
<form onSubmit={handleSubmit(handleStep3)}>
<input {...register('payment')} placeholder="Payment Method" />
<button type="button" onClick={() => setStep(2)}>Back</button>
<button type="submit">Submit</button>
</form>
)}
</div>
);
}Custom Data Tables
Using TanStack Table
'use client';
import { useDataTable } from '@/interfaces/data-table/hooks/use-data-table';
import { DataTable } from '@/interfaces/data-table/components/data-table';
export function CustomDataTable({ data }: { data: any[] }) {
const table = useDataTable({
data,
columns: [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<span className={`px-2 py-1 rounded ${
row.original.status === 'active' ? 'bg-green-100' : 'bg-red-100'
}`}>
{row.original.status}
</span>
),
},
{
id: 'actions',
cell: ({ row }) => (
<button onClick={() => handleEdit(row.original)}>
Edit
</button>
),
},
],
});
return <DataTable table={table} />;
}Custom Charts
Using Recharts
'use client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
import { useQuery } from '@tanstack/react-query';
import { getSupabaseBrowserClient } from '@/lib/supabase/clients/browser-client';
export function RevenueChart() {
const supabase = getSupabaseBrowserClient();
const { data } = useQuery({
queryKey: ['revenue-chart'],
queryFn: async () => {
const { data } = await supabase
.from('charts')
.select('*')
.eq('chart_name', 'daily_revenue');
return data;
},
});
return (
<LineChart width={800} height={400} data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="revenue" stroke="#8884d8" />
</LineChart>
);
}Server Actions
Create Server Action
'use server';
import { getSupabaseServerClient } from '@/lib/supabase/clients/server-client';
import { revalidatePath } from 'next/cache';
export async function createProduct(formData: FormData) {
const supabase = await getSupabaseServerClient();
const name = formData.get('name') as string;
const price = parseFloat(formData.get('price') as string);
const { error } = await supabase
.from('products')
.insert({ name, price });
if (error) {
return { error: error.message };
}
revalidatePath('/home/products');
return { success: true };
}Use in Component
'use client';
import { createProduct } from './actions';
import { useTransition } from 'react';
export function ProductForm() {
const [isPending, startTransition] = useTransition();
async function handleSubmit(formData: FormData) {
startTransition(async () => {
const result = await createProduct(formData);
if (result.error) {
alert(result.error);
}
});
}
return (
<form action={handleSubmit}>
<input name="name" required />
<input name="price" type="number" step="0.01" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Product'}
</button>
</form>
);
}Styling Custom Components
Using Tailwind CSS
export function StyledCard({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="space-y-4">
{children}
</div>
</div>
);
}Using Shadcn Components
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
export function ShippingForm() {
return (
<Card>
<CardHeader>
<CardTitle>Shipping Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input placeholder="Address" />
<Input placeholder="City" />
<Input placeholder="Postal Code" />
<Button>Save Address</Button>
</CardContent>
</Card>
);
}Adding to Navigation
To add custom pages to the navigation, modify components/layouts/default-sidebar.tsx:
// components/layouts/default-sidebar.tsx
import { Package } from 'lucide-react'; // Add your icon
const data = {
navMain: [
{
title: "Dashboard",
url: "/dashboard",
icon: HomeIcon,
},
{
title: "Chart",
url: "/chart",
icon: ChartBarIcon,
},
{
title: "Report",
url: "/report",
icon: FileChartColumnIcon,
},
],
navSecondary: [
{
title: "Storage",
url: "/home/storage",
icon: FolderIcon,
},
{
title: "Audit Logs",
url: "/home/audit-log",
icon: ScrollTextIcon,
},
// Add your custom navigation item here
{
title: "Custom Feature",
url: "/home/custom-feature",
icon: Package,
},
],
};The sidebar is structured with:
- NavMain: Primary navigation items (Dashboard, Chart, Report) - shown in header
- NavResources: Auto-generated resource navigation - shown in content
- NavSecondary: Secondary navigation items (Storage, Audit Logs) - shown in footer
Add your custom pages to navSecondary to display them in the sidebar footer.
Next Steps
- Theming - Customize appearance
- CRUD Operations - Leverage auto-generated UI