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 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
- 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.
- Load the user's tasks from the database.
- Display the tasks in a table.
- Use a search input to filter the tasks by title.
Let's take a look at the code:
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 { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
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 i18n = await createI18nServerInstance();
const title = i18n.t('account:homePage');
return {
title,
};
};
function UserHomePage(props: { searchParams: SearchParams }) {
const client = getSupabaseServerClient();
const { user } = use(loadUserWorkspace());
const page = parseInt(props.searchParams.page ?? '1', 10);
const query = props.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 withI18n(UserHomePage);
Let's break this down a bit, shall we:
- We import the necessary components and functions.
- We define the
SearchParams
interface to type the search parameters. - We define the
generateMetadata
function to generate the page metadata. - We define the
UserHomePage
component that loads the user's workspace and tasks from the database. - We define the
ServerDataLoader
component that loads the tasks from the database. - We render the tasks in a table and provide a search input to filter the tasks by title.
- We export the
UserHomePage
component with thewithI18n
HOC. This helps bootstrap the i18n instance for the component.
Tasks Table
Now, let's show the tasks table component:
'use client';
import Link from 'next/link';
import { ColumnDef } from '@tanstack/react-table';
import { Pencil } from 'lucide-react';
import { useTranslation } from 'react-i18next';
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 } = useTranslation('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 useTranslation
hook from the react-i18next
package to translate the column headers.