It's time to work on our application's value proposition: adding and tracking tasks! This is likely the most exciting part for you because it's where you get to change things and add your SaaS features to the template.
Page Structure
Before getting started, let's take a look at the default page structure of the boilerplate is the following.
├── pages
└── api
└── onboarding
└── organizations
└── stripe
└── session
└── user
└── auth
└── invite
└── [code].tsx
└── link.tsx
└── password-reset.tsx
└── sign-in.tsx
└── sign-up.tsx
└── dashboard
└── index.tsx
└── onboarding
└── index.tsx
└── settings
└── organization
└── members
└── index.tsx
└── invite.tsx
└── profile
└── index.tsx
└── email.tsx
└── password.tsx
└── authentication.tsx
└── subscription
└── blog
└── [collection]
└── [slug].tsx
└── docs
└── [page]
└── slug.tsx
└── index.tsx
└── 500.tsx
└── 404.tsx
└── _document.tsx
└── _app.tsx
└── index.tsx
└── faq.tsx
└── pricing.tsx
Setting the application's Home Page
By default, the application's home page is /dashboard
; every time the user logs in, they're redirected to the page src/pages/dashboard/index.tsx
.
You can update the above by setting the application's home page path at configuration.paths.appHome
. The configuration file can be found at src/configuration.ts
.
Unless you want to change the application's home page, you can skip this section, but it's good to know.
Demo: Tasks Application
Our demo application will be a simple tasks management application. We will be able to create tasks, list them, mark them as completed, and delete them.
Here is a quick demo of what we will build:
Loading video...
Routing: Adding the Tasks Pages
Ok, so we want to add three pages to our application:
- Tasks List: A page to list all our tasks
- Create New Task: Another page to create a new task
- Task Page: A page specific to the selected task
To create these two pages, we will create a folder named tasks
at src/pages/tasks.
In the folder src/pages/tasks
we will create three Page Components:
1. List Page
We create a page index.tsx
, which is accessible at the path /tasks
.
NB: we will be creating the TasksContainer
in the next steps.
import { GetServerSidePropsContext } from 'next';
import { withAppProps } from '~/lib/props/with-app-props';
import RouteShell from '~/components/RouteShell';
import TasksContainer from '~/components/tasks/TasksContainer';
import { useCurrentOrganization } from '~/lib/organizations/hooks/use-current-organization';
const Tasks = () => {
const organization = useCurrentOrganization();
if (!organization) {
return null;
}
return (
<RouteShell title={'Tasks'}>
<TasksContainer organizationId={organization.id} />
</RouteShell>
);
};
export default Tasks;
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
return await withAppProps(ctx);
}
Notice: we have added a getServerSideProps
function to the page. This is because we need to fetch the current organization of the user before rendering the page. We will be using this organization ID to fetch the tasks. Additionally, the withAppProps
function will:
- ensure the user is logged in
- ensure the user has an organization and is onboarded
As a rule of thumb, every page that requires the user to be logged in should use a getServerSideProps
function with withAppProps
.
import { GetServerSidePropsContext } from "next";
import { withAppProps } from "~/lib/props/with-app-props";
export async function getServerSideProps(
ctx: GetServerSidePropsContext
) {
return await withAppProps(ctx);
}
2. Create Task Page
We create a page new.tsx
, which is accessible at the path /tasks/new
. We will be creating the CreateTaskForm
component in the next steps.
import { GetServerSidePropsContext } from 'next';
import { withAppProps } from '~/lib/props/with-app-props';
import RouteShell from '~/components/RouteShell';
import CreateTaskForm from '~/components/tasks/CreateTaskForm';
const NewTaskPage = () => {
return (
<RouteShell title={'New Task'}>
<div
className={'max-w-2xl border border-gray-50 p-8 dark:border-black-400'}
>
<CreateTaskForm />
</div>
</RouteShell>
);
};
export default NewTaskPage;
export async function getServerSideProps(
ctx: GetServerSidePropsContext
) {
return await withAppProps(ctx);
}
3. Task Page
We create a page [id].tsx
, which is accessible at the path /tasks/<taskID>
where taskID
is a dynamic variable that refers to the actual ID of the task.
├── pages
└── tasks
└── index.tsx
└── new.tsx
└── [id].tsx
Below is what the page looks like. We will be defining the TaskItemContainer
component in the next steps.
import { GetServerSidePropsContext } from 'next';
import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon';
import { withAppProps } from '~/lib/props/with-app-props';
import TaskItemContainer from '~/components/tasks/TaskItemContainer';
import RouteShell from '~/components/RouteShell';
import Heading from '~/core/ui/Heading';
import Button from '~/core/ui/Button';
import Alert from '~/core/ui/Alert';
import ErrorBoundary from '~/core/ui/ErrorBoundary';
const TaskPage: React.FC<{ taskId: string }> = ({ taskId }) => {
return (
<RouteShell title={<TaskPageHeading />}>
<ErrorBoundary
fallback={<Alert type={'error'}>Ops, an error occurred :(</Alert>}
>
<TaskItemContainer taskId={taskId} />
</ErrorBoundary>
</RouteShell>
);
};
function TaskPageHeading() {
return (
<div className={'flex items-center space-x-6'}>
<Heading type={4}>
<span>Task</span>
</Heading>
<Button size={'small'} color={'transparent'} href={'/tasks'}>
<ArrowLeftIcon className={'mr-2 h-4'} />
Back to Tasks
</Button>
</div>
);
}
export default TaskPage;
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const appProps = await withAppProps(ctx);
const taskId = ctx.query.id;
if ('props' in appProps) {
return {
props: {
...(appProps.props ?? {}),
taskId,
},
};
}
return appProps;
}
Above, we're extending withAppProps
to include the taskId
in the props. This is just an example, it's not strictly needed as we would be able to fetch the task ID from the URL in the TaskItemContainer
component.
With that said, assuming you want to fetch data in the getServerSideProps
function, you can do so and then pass it to the component as props.
Adding Functionalities to your application
To add new functionalities to your application (in our case, tasks management), usually, you'd need the following things:
- Add a new page, and the links to it in
pages
- Add the components of the domain and import them into the pages in
components
- Add data-fetching and writing to this domain's entities in
lib
The above are the things we will do in the following few sections.
To clarify further the conventions of this boilerplate, here is what you should know:
- Data Model: first, we want to define the data model of the feature you want to add
- Firestore Hooks: once we're happy with the data model, we can create our Firestore hooks to write new tasks and then fetch the ones we created
- Import hooks into components: then, we import and use our hooks within the components
- Add feature components into the pages: finally, we add the components to the pages
Adding page links to the Navigation Menu
To update the navigation menu, we need to update the NAVIGATION_CONFIG
object in src/navigation.config.tsx
.
const NAVIGATION_CONFIG = {
items: [
{
label: 'common:dashboardTabLabel',
path: configuration.paths.appHome,
Icon: ({ className }: { className: string }) => {
return <Squares2X2Icon className={className} />;
},
},
{
label: 'common:settingsTabLabel',
path: '/settings',
Icon: ({ className }: { className: string }) => {
return <Cog8ToothIcon className={className} />;
},
},
],
};
To add a new link to the header menu, we can add the following item in the NAVIGATION_CONFIG
object:
{
label: 'common:tasksTabLabel',
path: '/tasks',
Icon: ({ className }: { className: string }) => {
return <Squares2X2Icon className={className} />;
},
},
Now, also add the common:tasksTabLabel
key to the public/locales/en/common.json
file:
{
"tasksTabLabel": "Tasks"
}
Save the file and restart the Next server by rerunning the npm run dev
command.
The result will be similar to the images below: