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
Supasheet uses TanStack Router with file-based routing. Add a new file under src/routes/ to create a new page.
Basic Custom Page
// src/routes/custom-feature/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase/client';
export const Route = createFileRoute('/custom-feature/')({
component: CustomFeaturePage,
});
function CustomFeaturePage() {
const { data: products } = useQuery({
queryKey: ['products'],
queryFn: async () => {
const { data } = await supabase.from('products').select('*').limit(10);
return data;
},
});
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 Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase/client';
export function DeleteButton({ id }: { id: string }) {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: async (id: string) => {
const { error } = await supabase.from('products').delete().eq('id', id);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
return (
<button onClick={() => mutate(id)} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}Custom Forms
Using TanStack Form
import { useForm } from '@tanstack/react-form';
import { supabase } from '@/lib/supabase/client';
export function CustomOrderForm() {
const form = useForm({
defaultValues: {
name: '',
email: '',
quantity: 1,
},
onSubmit: async ({ value }) => {
const { error } = await supabase.from('orders').insert(value);
if (error) console.error(error);
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
className="space-y-4"
>
<form.Field name="name">
{(field) => (
<div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Name"
/>
{field.state.meta.errors && (
<span className="text-red-500">{field.state.meta.errors[0]}</span>
)}
</div>
)}
</form.Field>
<form.Field name="email">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Email"
/>
)}
</form.Field>
<form.Field name="quantity">
{(field) => (
<input
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(Number(e.target.value))}
/>
)}
</form.Field>
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
);
}Multi-Step Form
import { useState } from 'react';
import { useForm } from '@tanstack/react-form';
export function MultiStepForm() {
const [step, setStep] = useState(1);
const step1 = useForm({
defaultValues: { name: '', email: '' },
onSubmit: () => setStep(2),
});
const step2 = useForm({
defaultValues: { address: '', city: '' },
onSubmit: async ({ value }) => {
// combine and submit
},
});
return (
<div>
{step === 1 && (
<form onSubmit={(e) => { e.preventDefault(); step1.handleSubmit(); }}>
<step1.Field name="name">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Name"
/>
)}
</step1.Field>
<button type="submit">Next</button>
</form>
)}
{step === 2 && (
<form onSubmit={(e) => { e.preventDefault(); step2.handleSubmit(); }}>
<step2.Field name="address">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Address"
/>
)}
</step2.Field>
<button type="button" onClick={() => setStep(1)}>Back</button>
<button type="submit">Submit</button>
</form>
)}
</div>
);
}Custom Data Tables
Using TanStack Table
import { useDataTable } from '@/components/data-table/hooks/use-data-table';
import { DataTable } from '@/components/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
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase/client';
export function RevenueChart() {
const { data } = useQuery({
queryKey: ['revenue-chart'],
queryFn: async () => {
const { data } = await supabase
.from('daily_revenue')
.select('*')
.order('date', { ascending: true });
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>
);
}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 sidebar, modify src/components/layouts/default-sidebar.tsx:
// src/components/layouts/default-sidebar.tsx
import { Package } from 'lucide-react';
const data = {
navMain: [
{
title: 'Dashboard',
url: '/dashboard',
icon: HomeIcon,
},
// ...
],
navSecondary: [
{
title: 'Storage',
url: '/storage',
icon: FolderIcon,
},
{
title: 'Audit Logs',
url: '/core/audit_logs',
icon: ScrollTextIcon,
},
// Add your custom navigation item here
{
title: 'Custom Feature',
url: '/custom-feature',
icon: Package,
},
],
};The sidebar is structured with:
- NavMain - Primary navigation (Dashboard, Chart, Report)
- NavResources - Auto-generated resource navigation
- NavSecondary - Secondary items (Storage, Audit Logs), add custom pages here
Next Steps
- Theming - Customize appearance