Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
231
docs/development/loading-data-from-database.mdoc
Normal file
231
docs/development/loading-data-from-database.mdoc
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
status: "published"
|
||||
label: "Loading data from the DB"
|
||||
order: 4
|
||||
title: "Learn how to load data from the Supabase database"
|
||||
description: "In this page we learn how to load data from the Supabase database and display it in our Next.js application."
|
||||
---
|
||||
|
||||
Now that our database supports the data we need, we can start loading it into our application. We will use the `@makerkit/data-loader-supabase-nextjs` package to load data from the Supabase database.
|
||||
|
||||
Please check the [documentation](https://github.com/makerkit/makerkit/tree/main/packages/data-loader/supabase/nextjs) for the `@makerkit/data-loader-supabase-nextjs` package to learn more about how to use it.
|
||||
|
||||
This nifty package allows us to load data from the Supabase database and display it in our server components with support for pagination.
|
||||
|
||||
In the snippet below, we will:
|
||||
|
||||
1. Load the user's workspace data from the database. This allows us to get the user's account ID without further round-trips because the workspace is loaded by the user layout.
|
||||
2. Load the user's tasks from the database.
|
||||
3. Display the tasks in a table.
|
||||
4. Use a search input to filter the tasks by title.
|
||||
|
||||
Let's take a look at the code:
|
||||
|
||||
```tsx
|
||||
import { use } from 'react';
|
||||
|
||||
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { TasksTable } from './_components/tasks-table';
|
||||
import { UserAccountHeader } from './_components/user-account-header';
|
||||
import { loadUserWorkspace } from './_lib/server/load-user-workspace';
|
||||
|
||||
interface SearchParams {
|
||||
page?: string;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const t = await getTranslations('account');
|
||||
const title = t('homePage');
|
||||
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
async function UserHomePage(props: { searchParams: Promise<SearchParams> }) {
|
||||
const client = getSupabaseServerClient();
|
||||
const { user } = use(loadUserWorkspace());
|
||||
|
||||
const searchParams = await props.searchParams;
|
||||
const page = parseInt(searchParams.page ?? '1', 10);
|
||||
const query = searchParams.query ?? '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserAccountHeader
|
||||
title={<Trans i18nKey={'common.homeTabLabel'} />}
|
||||
description={<Trans i18nKey={'common.homeTabDescription'} />}
|
||||
/>
|
||||
|
||||
<PageBody className={'space-y-4'}>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<div>
|
||||
<Heading level={4}>
|
||||
<Trans i18nKey={'tasks.tasksTabLabel'} defaults={'Tasks'} />
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center space-x-2'}>
|
||||
<form className={'w-full'}>
|
||||
<Input
|
||||
name={'query'}
|
||||
defaultValue={query}
|
||||
className={'w-full lg:w-[18rem]'}
|
||||
placeholder={'Search tasks'}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ServerDataLoader
|
||||
client={client}
|
||||
table={'tasks'}
|
||||
page={page}
|
||||
where={{
|
||||
account_id: {
|
||||
eq: user.id,
|
||||
},
|
||||
title: {
|
||||
textSearch: query ? `%${query}%` : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(props) => {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-8'}>
|
||||
<If condition={props.count === 0 && query}>
|
||||
<div className={'flex flex-col space-y-2.5'}>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={'tasks.noTasksFound'}
|
||||
values={{ query }}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<form>
|
||||
<input type="hidden" name={'query'} value={''} />
|
||||
|
||||
<Button variant={'outline'} size={'sm'}>
|
||||
<Trans i18nKey={'tasks.clearSearch'} />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<TasksTable {...props} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ServerDataLoader>
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserHomePage;
|
||||
```
|
||||
|
||||
Let's break this down a bit:
|
||||
|
||||
1. We import the necessary components and functions.
|
||||
2. We define the `SearchParams` interface to type the search parameters.
|
||||
3. We define the `generateMetadata` function to generate the page metadata.
|
||||
4. We define the `UserHomePage` component that loads the user's workspace and tasks from the database.
|
||||
5. We define the `ServerDataLoader` component that loads the tasks from the database.
|
||||
6. We render the tasks in a table and provide a search input to filter the tasks by title.
|
||||
7. We export the `UserHomePage` component.
|
||||
|
||||
### Displaying the tasks in a table
|
||||
|
||||
Now, let's show the tasks table component:
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { DataTable } from '@kit/ui/enhanced-data-table';
|
||||
|
||||
import { Database } from '~/lib/database.types';
|
||||
|
||||
type Task = Database['public']['Tables']['tasks']['Row'];
|
||||
|
||||
export function TasksTable(props: {
|
||||
data: Task[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
}) {
|
||||
const columns = useGetColumns();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTable {...props} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useGetColumns(): ColumnDef<Task>[] {
|
||||
const t = useTranslations('tasks');
|
||||
|
||||
return [
|
||||
{
|
||||
header: t('task'),
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
className={'hover:underline'}
|
||||
href={`/home/tasks/${row.original.id}`}
|
||||
>
|
||||
{row.original.title}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: t('createdAt'),
|
||||
accessorKey: 'created_at',
|
||||
},
|
||||
{
|
||||
header: t('updatedAt'),
|
||||
accessorKey: 'updated_at',
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
const id = row.original.id;
|
||||
|
||||
return (
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Link href={`/home/tasks/${id}`}>
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<Pencil className={'h-4'} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
In this snippet, we define the `TasksTable` component that renders the tasks in a table. We use the `DataTable` component from the `@kit/ui/enhanced-data-table` package to render the table.
|
||||
|
||||
We also define the `useGetColumns` hook that returns the columns for the table. We use the `useTranslations` hook from `next-intl` to translate the column headers.
|
||||
Reference in New Issue
Block a user