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 { createServerClient } from '@/lib/supabase/clients/server-client';
export default async function CustomFeaturePage() {
const supabase = createServerClient();
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 { createClient } from '@/lib/supabase/clients/client-client';
export function InteractiveComponent() {
const [data, setData] = useState([]);
const supabase = createClient();
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 { createClient } from '@/lib/supabase/clients/client-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 = createClient();
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 { createClient } from '@/lib/supabase/clients/client-client';
export function RevenueChart() {
const supabase = createClient();
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 { createServerClient } from '@/lib/supabase/clients/server-client';
import { revalidatePath } from 'next/cache';
export async function createProduct(formData: FormData) {
const supabase = createServerClient();
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>
);
}
Realtime Features
Subscribe to Changes
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@/lib/supabase/clients/client-client';
export function RealtimeOrders() {
const [orders, setOrders] = useState<any[]>([]);
const supabase = createClient();
useEffect(() => {
// Initial load
supabase
.from('orders')
.select('*')
.then(({ data }) => setOrders(data || []));
// Subscribe to changes
const channel = supabase
.channel('orders-channel')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'orders',
},
(payload) => {
if (payload.eventType === 'INSERT') {
setOrders((prev) => [payload.new, ...prev]);
} else if (payload.eventType === 'UPDATE') {
setOrders((prev) =>
prev.map((order) =>
order.id === payload.new.id ? payload.new : order
)
);
} else if (payload.eventType === 'DELETE') {
setOrders((prev) =>
prev.filter((order) => order.id !== payload.old.id)
);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [supabase]);
return (
<div>
<h2>Live Orders ({orders.length})</h2>
{orders.map((order) => (
<div key={order.id}>{order.status}</div>
))}
</div>
);
}
Integration with Third-Party APIs
Payment Integration
'use server';
import { createServerClient } from '@/lib/supabase/clients/server-client';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function createCheckoutSession(orderId: string) {
const supabase = createServerClient();
// Get order details
const { data: order } = await supabase
.from('orders')
.select('*, order_items(*)')
.eq('id', orderId)
.single();
if (!order) throw new Error('Order not found');
// Create Stripe session
const session = await stripe.checkout.sessions.create({
line_items: order.order_items.map((item: any) => ({
price_data: {
currency: 'usd',
product_data: { name: item.name },
unit_amount: item.price * 100,
},
quantity: item.quantity,
})),
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cancel`,
});
return { sessionId: session.id };
}
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
Update the sidebar:
// components/layouts/primary-sidebar.tsx
import { Package, BarChart, FileText, Settings, CustomIcon } from 'lucide-react';
const navigationItems = [
// ... existing items
{
label: 'Custom Feature',
path: '/home/custom-feature',
icon: <CustomIcon className="w-4 h-4" />,
},
];
Next Steps
- Internationalization - Add translations
- Theming - Customize appearance
- CRUD Operations - Leverage auto-generated UI