Supasheet.

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