Skip to content
4th April 2025: This is a preview, whilst production-ready, it means some APIs might change

Jobs List

The jobs application list page lists all the jobs that we’ve applied for. For this tutorial, it will also serve as the Dashboard.

The finished page will look like this:

Let’s start with the backend code and then make it look good.

But, first, we need a page and a route.

In the src > app > pages directory, create a new folder called applications. Inside, create a file called List.tsx.

  • Directorysrc/
    • Directoryapp/
      • Directorypages/
        • Directoryapplications/
          • List.tsx
        • Directoryauth/
        • Home.tsx

We can stub out a basic page, just to make sure it’s loading correctly.

src/app/pages/applications/List.tsx
const List = () => {
return (
<div>List</div>
)
}
export { List }

Now, within our worker.tsx file, we can add a route for our new page.

src/worker.tsx
import { List } from "./app/pages/applications/List";
...
prefix("/applications", [
route("/", List),
])
  • We'll group all of our application routes (List, New, Detail, Update) under the /applications prefix.
  • When the user visits /applications, they'll see our List component.

Let’s test this out in the browser. Go to http://localhost:2332/applications and you should see our stubbed out page.

Since we’ve already set up authentication, we can also protect this route.

prefix("/applications", [
route("/", [
({ ctx }) => {
if (!ctx.user) {
return new Response(null, {
status: 302,
headers: { Location: "/user/login" },
});
}
}, List]),
])

This would get cumbersome (and annoying) if we have to do this for every.single.route we wanted to protect. Let’s abstract this code into a reusable function.

const isAuthenticated = ({ ctx }: { ctx: AppContext}) => {
if (!ctx.user) {
return new Response(null, {
status: 302,
headers: { Location: "/user/login" },
});
}
}

We're passing in the ctx that we get with each request. Then, we're checking to see if the user exists on the context object. If it doesn't, we're returning a response that redirects the user to the login page.

Then, we can update our /applications route to use isAuthenticated:

prefix("/applications", [
route("/", [isAuthenticated, List]),
])

Now, we can also update our protected page route to use the isAuthenticated function, too:

route("/protected", [ isAuthenticated, Home ]),

Actually, let’s refactor this. The protected route was really meant as an example. We don’t need /protected, but we do want to protect our home page: route("/", Home),. We could change /protected to /, and then delete route("/", Home),.

route("/", [ isAuthenticated, Home ]),

I want to take this a step further, though, and show you another option. With the homepage route, you can also use the index function. This works very similar to the route function, but it already knows the path is /, so it only takes one argument: the response.

index([ isAuthenticated, Home ]),

Be sure to import index at the top of the file:

import { route, render, prefix, index } from "@redwoodjs/sdk/router";

Test it out. 👨‍🍳 Chef’s kiss! If you’re logged in, you should see the “List” text. If you’re not logged in, you’ll be redirected to the login page.

Now, let’s get some data into the database.

We can do this one of two ways:

Option 1: Create a Seed File

Earlier, I mentioned that sometimes I’ll create multiple seed files with various purposes.. This is a perfect opportunity to create a separate file just for adding job applications to our database.

Inside the src/scripts directory, create a new file called applicationSeed.ts.

Let’s stub it out:

src/scripts/applicationSeed.ts
import { defineScript } from "@redwoodjs/sdk/worker";
import { db, setupDb } from "@/db";
export default defineScript(async ({ env }) => {
setupDb(env);
console.log("🌱 Finished seeding");
});
  • On line 4, we're setting up the Cloudflare Worker environment to run our script. By default, we get the env object.
  • On line 5, we're setting up our database.
  • On line 7, we're logging a message to the console to indicate that the script has finished running.

Inside our function, we can reach for a standard Prisma create function:

src/scripts/applicationSeed.ts
const createApplication = async () => {
await db.application.create({
data: {
salaryMin: "100000",
salaryMax: "120000",
jobTitle: "Software Engineer",
jobDescription: "Software Engineer",
postingUrl: "https://redwoodjs.com",
dateApplied: new Date(),
}
}
};
await createApplication();
  • On line 8, we're referencing the application table. Then, using the Prisma create function to add all the data in our database object. You'll notice that these values match the columns we defined in Prisma.schema file.

You’ll probably see a few linting errors.

If you run the script now, you’ll hit a few errors because it’s also looking for related user, status, and company entries. For the user and status connections, we already have entries within the database we can reference.

Let’s start with the user connection. If you look at the user table, you’ll see that their ID is 0f9a097c-d7bc-4ab5-8b11-6942163df348. (Obviously, yours will be slightly different.) Copy that value.

Now, we can connect the entries, by setting user to an object with a connect key. Inside, we’ll specify the id of the user we want to connect to.

src/scripts/applicationSeed.ts
export default defineScript(async ({ env }) => {
setupDb(env);
const createApplication = async () => {
await db.application.create({
data: {
user: {
connect: {
id: "0f9a097c-d7bc-4ab5-8b11-6942163df348",
},
},
...

We want to do something similar for the status. If we look at the ApplicationStatus table, you’ll notice that an id of 1 is associated with a New application.

We can connect the application record to the status record, by referencing an object with a connect key that contains an object with an id of 1.

src/scripts/applicationSeed.ts
export default defineScript(async ({ env }) => {
setupDb(env);
const createApplication = async () => {
await db.application.create({
data: {
...
applicationStatus: {
connect: {
id: 1,
},
},
...

The company field is a little different because we haven’t created any company records yet. However, we can create and connect a company record at the same time:

src/scripts/applicationSeed.ts
export default defineScript(async ({ env }) => {
setupDb(env);
const createApplication = async () => {
await db.application.create({
data: {
...
company: {
create: {
name: "RedwoodSDK",
contacts: {
create: {
firstName: "John",
lastName: "Doe",
role: "Hiring Manager",
},
},
},
},
...

This time instead of using an object with a connect key, we’ll use a create key inside. Then, we can list an object with all the company’s data.

Complete applicationSeed.ts file
src/scripts/applicationSeed.ts
import { defineScript } from "@redwoodjs/sdk/worker";
import { db, setupDb } from "@/db";
export default defineScript(async ({ env }) => {
setupDb(env);
const createApplication = async () => {
await db.application.create({
data: {
user: {
connect: {
id: "0f9a097c-d7bc-4ab5-8b11-6942163df348", // REPLACE THIS WITH YOUR USER ID
},
},
applicationStatus: {
connect: {
id: 1,
},
},
company: {
create: {
name: "RedwoodSDK",
contacts: {
create: {
firstName: "John",
lastName: "Doe",
role: "Hiring Manager",
},
},
},
},
salaryMin: "100000",
salaryMax: "120000",
jobTitle: "Software Engineer",
jobDescription: "Software Engineer",
postingUrl: "https://redwoodjs.com",
dateApplied: new Date(),
}
});
};
await createApplication();
console.log("🌱 Finished seeding");
});

To run the seed file, within the Terminal:

Terminal window
pnpm worker:run ./src/scripts/applicationSeed.ts

If this feels hard to remember, you can create a script in your package.json file. Inside the scripts block:

package.json
...
"scripts": {
12 collapsed lines
"build": "vite build",
"dev": "NODE_ENV=${NODE_ENV:-development} vite dev",
"dev:init": "rw-scripts dev-init",
"worker:run": "rw-scripts worker-run",
"clean": "pnpm build && pnpm clean:vendor",
"clean:vite": "rm -rf ./node_modules/.vite",
"clean:vendor": "rm -rf ./vendor/dist",
"release": "pnpm build && wrangler deploy",
"format": "prettier --write ./src",
"migrate:dev": "wrangler d1 migrations apply DB --local",
"migrate:prd": "wrangler d1 migrations apply DB --remote",
"migrate:new": "rw-scripts migrate-new",
"seed": "pnpm worker:run ./src/scripts/seed.ts",
"seed:applications": "pnpm worker:run ./src/scripts/applicationSeed.ts",
"icons": "lemon-lime-svgs"
},
...

Now, you can run the seed file by saying:

Terminal window
pnpm seed:applications

😅 Much more straight forward!

When you’re creating custom seed files, this does take more time on the frontend to set up. But, it makes it much easier in the long run. Now, anytime you need a fresh set of data, you can run the seed file.

Option 2: Prisma Studio

Another option is to use Prisma Studio. This option is easier, but requires manual entry and can take more time in the long run. (We already set up Prisma Studio, here.)

Terminal window
npx prisma studio

Prisma Studio should now be available at http://localhost:5555. From here, you can create, read, update, and delete records.

Displaying the Job Application Data

Once you’ve added some data, let’s go back to our application and display the data on the page.

On the Applications list page, let’s import the db at the top of the file. Then, inside the List function, let’s use the findMany function to get all the applications:

src/app/pages/applications/List.tsx
import { db } from "src/db";
const List = async () => {
const applications = await db.application.findMany();
return (
<div>
<pre>{JSON.stringify(applications, null, 2)}</pre>
</div>
)
}
export { List }

Notice, we also made our List function an async function. We need to await the database call before rendering the page.

All the data from the database is returned as an array and saved in a variable called applications. Then, we can display all the data by using the JSON.stringify function. Wrapping our code in pre tags, it makes it easier to read.

Easy, right?! Since we’re using React Server Components, this code runs on the server. We’re able to make database calls directly from the page and don’t need to worry about creating API routes. 🎉

Also, if you look right click on the page and select View Source, you’ll see that the data is being rendered directly on the page.

Now that we know the content from our database is getting on the page, let’s style it.

Styling the Job Applications List Page

Similar to the auth pages, let’s start by creating a layout that will wrap all of our interior pages.

Creating an Interior Page Layout

Inside our layouts folder, let’s create a new file called InteriorLayout.tsx.

  • Directorysrc/
    • Directoryapp/
      • Directorylayouts/
        • AuthLayout.tsx
        • InteriorLayout.tsx

Inside, we need should use some of the same the same styles that we used within our AuthLayout.tsx file. As a quick reminder, let’s take a look at the AuthLayout.tsx file:

src/app/layouts/AuthLayout.tsx
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="bg-bg min-h-screen min-w-screen p-12">
<div className="grid grid-cols-2 min-h-[calc(100vh-96px)] rounded-xl border-2 border-[#D6D5C5]">
...
  • The wrapping div sets the background color, the minimum height and width of the page, and adds some padding.
  • The child div sets up the grid, applies rounded corners, and adds a border.

We don’t need to set up a grid, but we can abstract the styles and reuse them within our interior layout.

From the AuthLayout.tsx file, I’m going to copy the bg-bg min-h-screen min-w-screen p-12 styles and create a class inside our styles.css file, inside the @layer components block:

src/styles.css
.page-wrapper {
@apply bg-bg min-h-screen min-w-screen p-12;
}

Then, let’s do something similar with the child div from our AuthLayout.tsx. We don’t need the grid, but we can grab everything else: min-h-[calc(100vh-96px)] rounded-xl border-2 border-[#D6D5C5]

src/styles.css
.page {
@apply min-h-[calc(100vh-96px)] rounded-xl border-2 border-[#D6D5C5];
}
@layer components section of styles.css
src/styles.css
@layer components {
.page {
@apply min-h-[calc(100vh-96px)] rounded-xl border-2 border-[#D6D5C5];
}
.page-wrapper {
@apply bg-bg min-h-screen min-w-screen p-12;
}
.page-title {
@apply text-3xl
}
}

Let’s also add our route to our links.ts file for type hinting:

src/app/shared/links.ts
import { defineLinks } from "@redwoodjs/sdk/router";
export const link = defineLinks([
6 collapsed lines
"/",
"/user/login",
"/user/signup",
"/user/logout",
"/legal/privacy",
"/legal/terms",
"/applications",
]);

Now, let’s update the class list within our AuthLayout.tsx file:

src/app/layouts/AuthLayout.tsx
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="page-wrapper">
<div className="grid grid-cols-2 page">
...

Now, let’s jump over to our InteriorLayout.tsx file and use these classes there as well:

src/app/layouts/InteriorLayout.tsx
const InteriorLayout = ({ children }: { children: React.ReactNode}) => {
return (
<div className="page-wrapper">
<main className="page bg-white">
{children}
</main>
</div>
)
}
export { InteriorLayout }

You'll also notice I added a background of white with bg-white

To see how this looks, we need to add this to our List.tsx:

src/app/pages/applications/List.tsx
return (
<InteriorLayout>
<pre>{JSON.stringify(applications, null, 2)}</pre>
</InteriorLayout>
)

It’s coming together! Across the top, let’s add the logo and navigation. We may need to reuse this component in the future, so let’s make it it’s own component. Inside the components folder, let’s create a new file called Header.tsx.

  • Directorysrc/
    • Directoryapp/
      • Directorycomponents/
        • Header.tsx
src/app/components/Header.tsx
const Header = () => {
return (
<header>
{/* left side */}
<div></div>
{/* right side */}
<div></div>
</header>
)
}

Here are the basic building blocks we need.

  • I used a semantic HTML element header to wrap everything.
  • Then, we'll have a left and right side. On the left, we'll display the logo and the navigation. On the right, we'll display a link the user's settings, the logout button, and the user's avatar.

Mockup from Figma

Inside the left div, let’s add the logo and “Apply Wize” text and wrap it in a link:

src/app/components/Header.tsx
{/* left side */}
<div>
<a href={link("/")}>
<img src="/images/logo.svg" alt="Apply Wize" />
<span>Apply Wize</span>
</a>
</div>
  • Pretty straightforward. We’re using the same logo.svg that we used on the auth pages. It should already be in your public/images folder.
  • Then, we’re using the link helper to link to home page. At the top of your file, you’ll need to import the link helper:
import { link } from '@/app/shared/links'

Now, let’s add the navigation. For now this is only one link that goes to the dashboard page.

src/app/components/Header.tsx
{/* left side */}
<div>
4 collapsed lines
<a href={link("/")}>
<img src="/images/logo.svg" alt="Apply Wize" />
<span>Apply Wize</span>
</a>
<nav>
<ul>
<li><a href={link("/applications")}>Dashboard</a></li>
</ul>
</nav>
</div>

For the right side, we want another unordered list and the Avatar component:

src/app/components/Header.tsx
{/* right side */}
<nav>
<ul>
<li><a href="#">Settings</a></li>
<li><a href={link("/user/logout")}>Logout</a></li>
<li>
<Avatar>
<AvatarFallback>R</AvatarFallback>
</Avatar>
</li>
</ul>
</nav>
  • Notice, I replaced the right side div with a nav element, keeping things nice and semantic.
  • Inside, I’ve included links for the settings and logout pages.
  • For the Avatar component, you’ll need to import it at the top of your file. This is a shadcn/ui component, so it should already be part of your project.
import { Avatar, AvatarFallback } from '@/app/components/ui/avatar'

Normally, when you’re using the Avatar component, you’ll also want to use the AvatarImage component. This is where you define the Avatar image:

src/app/components/Header.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/app/components/ui/avatar'
...
<li>
<Avatar>
<AvatarFallback>R</AvatarFallback>
<AvatarImage src="./images/avatar.png" />
</Avatar>
</li>

If you want to go this route, you can download the avatar.png placeholder image and hard code the image source. However, we’re not going to cover file uploads and storage in this tutorial. So, we’ll use the AvatarFallback component to display the first character of the username.

The shadcn/ui Avatar component uses client side interactivity. So, you’ll need to add the use client directive to the top of the Avatar.tsx file:

src/app/components/ui/avatar.tsx
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/app/lib/utils"
function Avatar({
...

If you forget to add the use client directive, you’ll see a “null reading useState” error in the browser:

Now that we have all the elements, let’s add some styling:

src/app/layouts/Header.tsx
<header className="py-5 px-10 h-20 flex justify-between items-center border-b-1 border-border mb-12">
  • py-5 adds 20px of padding to the top and bottom.
  • px-10 adds 40px of padding to the left and right.
  • h-20 sets the height to 80px.
  • flex and justify-between are used to align the items inside the header, putting as much space between each of the elements as possible.
  • items-center centers the items vertically.
  • border-border and border-b-1 adds a border to the bottom of the header with a color of border (defined as a custom color in the @theme block of our styles.css file).

We set the left and right padding to 40px with px-10. We’ll use this throughout our entire application. In order to maintain consistency, let’s define it as a custom utility. This will make it easy to reference (and change, if necessary).

src/styles.css
--spacing-page-side: 40px;

Inside the @theme block, below our color definitions, we'll add a new variable called --spacing-page-side and set it to 40px. Now, we can use this variable with margin or padding: mx-page-side or px-page-side respectively.

Now, we can update our header element to use the new utility, replacing px-10 with px-page-side:

src/app/layouts/Header.tsx
<header className="py-5 px-page-side h-20 flex justify-between items-center border-b-1 border-border mb-12">

On the left side div, we want the logo and the Dashboard link to align vertically:

src/app/layouts/Header.tsx
{/* left side */}
<div className="flex items-center gap-8">
9 collapsed lines
<a href={link("/")}>
<img src="/images/logo.svg" alt="Apply Wize" />
<span>Apply Wize</span>
</a>
<nav>
<ul>
<li><a href={link("/applications")}>Dashboard</a></li>
</ul>
</nav>
</div>
  • We're using flex and items-center to align the items vertically.
  • gap-8 adds 32px of space between the logo and the Dashboard link.

On the home page link, we want the logo and the “Apply Wize” text to align vertically too:

src/app/components/Header.tsx
<a href={link("/")} className="flex items-center gap-3 font-display font-bold text-3xl">
  • flex, items-center, and gap-3 aligns the logo and text and puts 12px of space between them.
  • font-display and font-bold are used to style the text, applying the font Poppins and making the text bold.
  • text-3xl sets the font size to 30px

If you look at the logo, it overlaps with the bottom border of the header.

In order to achieve this, we also need to add some styles to the img tag:

src/app/components/Header.tsx
<img src="/images/logo.svg" alt="Apply Wize" className="pt-5 -mb-3" />
  • pt-5 adds 20px of padding to the top.
  • -mb-3 removes 12px of margin from the bottom and will make the bottom of the header shift up.

For the right side ul, we need to add a few styles to position the links properly:

src/app/components/Header.tsx
{/* right side */}
<nav>
<ul className="flex items-center gap-7">
...
  • Similar to techniques we've used before, we're using flex and items-center to align the items vertically.
  • gap-7 adds 28px of space between each of the links.

To style our nav links, I want these styles to apply to both the left and the right side. So, let’s stick these inside the styles.css file, inside the @layer base block:

src/styles.css
nav {
@apply font-display font-medium text-sm;
}

That should be it! (for the header at least)

Finished Header.tsx component
src/app/components/Header.tsx
import { link } from '@/app/shared/links'
import { Avatar, AvatarFallback } from './ui/avatar'
const Header = () => {
return (
<header className="py-5 px-page-side h-20 flex justify-between items-center border-b-1 border-border mb-12">
{/* left side */}
<div className="flex items-center gap-8">
<a href={link("/")} className="flex items-center gap-3 font-display font-bold text-3xl">
<img src="/images/logo.svg" alt="Apply Wize" className="pt-5 -mb-3" />
<span>Apply Wize</span>
</a>
<nav>
<ul>
<li><a href={link("/applications")}>Dashboard</a></li>
</ul>
</nav>
</div>
{/* right side */}
<nav>
<ul className="flex items-center gap-7">
<li><a href="#">Settings</a></li>
<li><a href={link("/user/logout")}>Logout</a></li>
<li>
<Avatar>
<AvatarFallback>R</AvatarFallback>
</Avatar>
</li>
</ul>
</nav>
</header>
)
}
export { Header }

Now, let’s stick our Header component into our InteriorLayout.tsx file:

src/app/layouts/InteriorLayout.tsx
import { Header } from "@/app/components/Header";
const InteriorLayout = ({ children }: { children: React.ReactNode}) => {
return (
<div className="page-wrapper">
<main className="page bg-white">
<Header />
<div>{children}</div>
</main>
</div>
)
}
export { InteriorLayout }

Check it out in the browser!

Moving on.

At the top of our file, let’s add a page heading and a button/link to add a new application:

src/app/pages/applications/List.tsx
import { Button } from "@/app/components/ui/button";
...
return (
<InteriorLayout>
<>
<div>
<h1>All Applications</h1>
<div>
<Button asChild><a href="#">New Application</a></Button>
</div>
</div>
<pre>{JSON.stringify(applications, null, 2)}</pre>
</>
</InteriorLayout>
)
  • InteriorLayout can only have one react node, so we need to wrap everything with a React fragment (<>)
  • Above our application content, I added a wrapping div with a h1 heading for “All Applications”.
  • Then, I have another div that wraps a <Button> component. Inside, I have a link that points to the new application page. The Button component is coming from shadcn/ui. It should already be part of your project, but you’ll need to import it at the top of your file.
  • Since we’re not triggering an event, we’re linking to another page, we have an a tag. Eventually, this will reference the applications/new route, but since we haven’t set that up yet, I used a placeholder # instead.

Now, let’s add some styling:

src/app/pages/applications/List.tsx
<div className="px-page-side flex justify-between items-center">
<h1 className="page-title">All Applications</h1>
  • On the wrapping div we can use our custom px-page-side that adds 20px of padding to the left and right.
  • We can align the heading and the button withflex, justify-between, and items-center.
  • We can use our page-title class to style the heading.

Styling the Applications Table

Now, let’s style the list of applications.

Inside our components directory, let’s create a new file, called ApplicationsTable.tsx.

  • Directorysrc/
    • Directoryapp/
      • Directorycomponents/
        • ApplicationsTable.tsx

I’m going to stub out a basic component:

src/app/components/ApplicationsTable.tsx
const ApplicationsTable = () => {
return (
<div>ApplicationsTable</div>
)
}
export { ApplicationsTable }

Let’s go ahead and put this on our ApplicationsList.tsx page, so we can see the updates we’re making in the browser:

src/app/pages/applications/List.tsx
import { ApplicationsTable } from "@/app/components/ApplicationsTable";
...
return (
<InteriorLayout>
<>
<div className="px-page-side flex justify-between items-center">
<h1 className="page-title">All Applications</h1>
<div>
<Button asChild><a href="#">New Application</a></Button>
</div>
</div>
<ApplicationsTable />
<pre>{JSON.stringify(applications, null, 2)}</pre>
</>
</InteriorLayout>
)

We’ve already added the shadcn/ui Table component to our project, so let’s go back to our ApplicationsTable.tsx file and use it. Just to start, I’m going to copy the example code from the shadcn/ui documentation and then we can rework it for our needs:

src/app/components/ApplicationsTable.tsx
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"
const ApplicationsTable = () => {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>
)
}
export { ApplicationsTable }

It looks pretty good, but we need to make a few changes to make it work for our data.

First, we can get rid of the TableCaption:

<TableCaption>A list of your recent invoices.</TableCaption>

Then, I’m going to change the TableHeader to match our design within Figma:

src/app/components/ApplicationsTable.tsx
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Status</TableHead>
<TableHead>Date Applied</TableHead>
<TableHead>Job Title</TableHead>
<TableHead>Company</TableHead>
<TableHead>Contact</TableHead>
<TableHead>Salary Range</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
  • We now have columns for status, date applied, job title, company, contact, and salary range.
  • You’ll notice that the last column is empty. <TableHead></TableHead> Within the table body, this will be our view icon, but this column doesn’t have a header.

For our TableBody, we need our application data. First, let’s set up our table row, statically. Then, we’ll make it dynamic.

src/app/components/ApplicationsTable.tsx
<TableBody>
<TableRow>
<TableCell>New</TableCell>
<TableCell>April 15, 2025</TableCell>
<TableCell>Software Engineer</TableCell>
<TableCell>RedwoodJS</TableCell>
<TableCell>John Doe</TableCell>
<TableCell>$150,000-$250,000</TableCell>
<TableCell><a href="#">View</a></TableCell>
</TableRow>
</TableBody>

Cool. Let’s make a few stylistic changes before we plug in the data.

Adding Badges to the Status Column

The status column should be a badge. We can reach for the Badge component from shadcn/ui.

src/app/components/ApplicationsTable.tsx
import { Badge } from "./ui/badge"
...
<TableCell>
<Badge>New</Badge>
</TableCell>

Let’s add styles based on the application status. We’ve already added some custom colors to our styles.css file, so let’s use those.

--color-tag-applied: #b1c7c0;
--color-tag-interview: #da9b7c;
--color-tag-new: #db9a9f;
--color-tag-rejected: #e4e3d4;
--color-tag-offer: #aae198;

Inside of our badge.tsx component, there’s a section at the top of variants:

src/app/components/ui/badge.tsx
variants: {
variant: {
8 collapsed lines
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
applied: "bg-tag-applied text-black",
interview: "bg-tag-interview text-black",
new: "bg-tag-new text-black",
rejected: "bg-tag-rejected text-black",
offer: "bg-tag-offer text-black",
},
},

I added a custom variant for applied, interview, new, rejected, and offer.

While, we’re here, I’m also going to add a few more classes to the default styling:

src/app/components/ui/badge.tsx
const badgeVariants = cva(
"font-bold inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
  • I added a font weight of font-bold
  • I changed the rounded corners to rounded-full

Let’s go back to our ApplicationsTable.tsx component and update our badge:

src/app/components/ApplicationsTable.tsx
<TableCell><Badge variant="new">New</Badge></TableCell>

For the contact column, let’s include an avatar:

src/app/components/ApplicationsTable.tsx
<TableCell>
<Avatar>
<AvatarFallback>J</AvatarFallback>
</Avatar>
John Doe
</TableCell>

This is the same avatar component we used in the Header component.

Be sure to import the Avatar and AvatarFallback components at the top of your file:

src/app/components/ApplicationsTable.tsx
import { Avatar, AvatarFallback } from "./ui/avatar"

Let’s add a few styles to fix the positioning:

src/app/components/ApplicationsTable.tsx
<TableCell className="flex items-center gap-2">
<Avatar>
<AvatarFallback>J</AvatarFallback>
</Avatar>
John Doe
</TableCell>
  • flex and items-center aligns the avatar and name vertically.
  • gap-2 adds 8px of padding between the avatar and the name.

Next, let’s replace the “View” text with an SVG icon.

Using an SVG Icon

We have several icons we want to use throughout our application. You can export all the SVGs directly from the Figma file, or you can download all of them within this project’s assets directory.

I’m going to create a new folder in the root of our our project called other and then a sub directory inside that called svg-icons and place all of the icons inside the svg-icons directory.

  • Directoryother/
    • Directorysvg-icons/
  • Directorysrc/

My favorite way to implement SVG icons is through an SVG sprite. Basically, this combines all our SVG files into a single sprite.svg file. We can control which icon is displayed by setting the id attribute on the use element.

You could set all of this up, manually, but I do this on every single project I build, so I’ve built an npm package to do all the heavy lifting. It’s called Lemon Lime SVGs.

Within the Terminal run:

Terminal window
pnpm install lemon-lime-svgs --save-dev

Once installed, run the setup command:

Terminal window
npx lemon-lime-svgs setup

This will ask a series of questions:

  • First, it will ask you what framework you’re using. At it’s core RedwoodSDK is React and Vite = 6
  • Next, it will ask you about file names and folder paths. It will make recommendations based on the framework you’re using. Most of the defaults, work:
    • Input directory for SVG files: ./other/svg-icons — we’ve already set this directory up!
    • Output directory for sprite: ./public/images/icons This is the same location as the background and logo.svg
    • Directory for TypeScript types: ./src/types
    • Sprite file name: sprite.svg
    • Type definition file name: icons.d.ts
    • Enable verbose logging: The default is set to “no”, but I usually like to turn this on and get a little extra feedback.
    • Generate a README. The default is set to “yes”, but I usually like to turn this off. The README lives inside the same directory as your sprite and tells future developers that this file was created programmatically. It also provides a list of all the SVG icons available.
  • These settings are saved inside your package.json file, in its own section called, lemonLimeSvgs.
  • This script will also create a new script command inside your package.json file, called icons. Once we add the icons to our svg-icons folder, we can generate the sprite using this command: pnpm run icons.
  • The last prompt asks us if we want to add an Icon component to our project. Say y.
  • Then, it will ask us where we want to save our component. We need to veer from the recommendation slightly: src/app/components/Icon.tsx

Now, if you look inside your src/app/components directory, you’ll see a new Icon.tsx file.

src/app/components/Icon.tsx
interface Props {
size?: number;
id: string;
className?: string;
}
const Icon = ({ className, size = 24, id }: Props) => {
return (
<svg width={size} height={size} className={className}>
<use href={`/images/icons/sprite.svg#${id}`}></use>
</svg>
);
};
export default Icon;

This component takes a className, if you want to add additional styles to the component, a size (the default is set to 24px), and the id of the icon you want to display. The id matches the file name of the original icon SVG file.

Before we move on, I’m going to change this to a named export to be consistent with the other components we’ve created:

src/app/components/Icon.tsx
export { Icon }

Now, let’s take all our icon SVGs and dump them inside our other/svg-icons directory.

Inside the terminal, let’s generate our sprite:

Terminal window
pnpm icons

Sweet! Now we can use our Icon component.

Inside our ApplicationsTable.tsx file:

src/app/components/ApplicationsTable.tsx
import { Icon } from "./Icon"
...
<TableCell>
<a href="#">
View
<Icon id="view" />
</a>
</TableCell>

Making our Table Dynamic

Awesome! Now, let’s make it dynamic. Inside our List.tsx page, let’s pass in all of our application data:

src/app/pages/applications/List.tsx
<ApplicationsTable applications={applications} />

We already have our applications saved inside a variable called applications, which makes passing in all our data, easy.

When you first set this up, you’ll probably see an error

Our component isn’t expecting the applications data we’re passing it. Inside our ApplicationsTable component:

src/app/components/ApplicationsTable.tsx
import { Application } from "@prisma/client"
...
const ApplicationsTable = ({ applications }: { applications: Application[] }) => {
  • We’ll set it up to receive the applications data as a prop.
  • Then, we need to set the type. Fortunate for us, Prisma generates these for us. So, we can use the Application type and since there’s an array of them, we’ll use square brackets [].
  • At the top of our, file we can import the Application type from @prisma/client.

Now, let’s go down to our TableBody. First we want to loop over all the applications within our applications array and display a row in the table for each:

src/app/components/ApplicationsTable.tsx
<TableBody>
{applications.map(application => (
<TableRow>
<TableCell><Badge>New</Badge></TableCell>
<TableCell>April 15, 2025</TableCell>
<TableCell>Software Engineer</TableCell>
<TableCell>RedwoodJS</TableCell>
<TableCell className="flex items-center gap-2">
<Avatar>
<AvatarFallback>J</AvatarFallback>
</Avatar>
John Doe
</TableCell>
<TableCell>$150,000-$250,000</TableCell>
<TableCell>
<a href="#">
<Icon id="view" />
</a>
</TableCell>
</TableRow>
))}
</TableBody>

Now, we can start replacing the static content with dynamic content.

For our application status and our Badge component, we can swap out the new text with {application.applicationStatus.status}.

src/app/components/ApplicationsTable.tsx
<TableCell>
<Badge variant={application.applicationStatus.status}>
{application.applicationStatus.status}
</Badge>
</TableCell>

I’m starting to get an error within my code editor.

Basically, it’s saying that it’s not aware of the applicationStatus relationship.

Setting up Table Relationships

Let’s go back to our List.tsx file. On line 6, we’re getting all the applications from our table:

src/app/pages/applications/List.tsx
const applications = await db.application.findMany();

But, we also need it to include the applicationStatus, company, and contact relationships. Prisma has an easy want to do this using include:

src/app/pages/applications/List.tsx
const applications = await db.application.findMany({
include: {
applicationStatus: true,
company: {
include: {
contacts: true
}
}
}
});
  • On line 8 we're telling it to incldue the applicationStatus relationship.
  • On line 9 we're telling it to include the company relationship.
  • The contact relationship is a little different because it's through the company relationship, but we can continue to drill down, including the contacts relationship on line 11.

You might need to temporarily comment out the Badge component inside the ApplicationsTable.tsx file, to avoid errors, but if you take a look at the result within the browser, you’ll see the JSON data change shape:

Now, if we go back to our ApplicationsTable.tsx file, the TypeScript error is still there. We’re getting the applicationStatus information now, but we need to make sure the type is updated to include it.

We’re already getting the Application type from @prisma/client. We can also import ApplicationStatus, Company, and Contact types.

Then, let’s change the shape of our object. We have our Application type, then, we can merge an object & that has applicationStatus and company. Contacts is an array, nested inside the company object.

src/app/components/ApplicationsTable.tsx
const ApplicationsTable = ({ applications }: {
applications: Application[]
applications: (Application & {
applicationStatus: ApplicationStatus,
company: Company & {
contacts: Contact[]
},
})[]

If you look at your TableCell again, you’re probably still seeing an error 🤯

src/app/components/ApplicationsTable.tsx
<TableCell>
<Badge variant={application.applicationStatus.status}>
{application.applicationStatus.status}
</Badge>
</TableCell>

When we pass in the variant prop for our Badge component, it’s not just looking for a string, it’s looking for a specific string: "default" | "secondary" | "destructive" | "outline" | "applied" | "interview" | "new" | "rejected" | "offer" | null | undefined

Inside our badge component, when we defined our badgeVariants for styles, it used our variant options to create a type definition. badgeVariants is exported from the badge.tsx component, making it easy to reuse and reference within our ApplicationsTable.tsx:

src/app/components/ApplicationsTable.tsx
<Badge variant={application.applicationStatus?.status.toLowerCase() as VariantProps<typeof badgeVariants>['variant']}>
{application.applicationStatus?.status}
</Badge>
  • On the application.applicationStatus?.status, we can append .toLowerCase() to make sure the status is formatted correctly and always lowercase.
  • typeof badgeVariants - Gets the type of our badgeVariants configuration object, created with cva(). This includes all our variant definitions.
  • VariantProps<...> - This is a utility type from class-variance-authority that extracts the prop types. It creates a type that includes all possible variants as properties.
  • ['variant'] - This accesses just the variant property type from those props. In our case, it resolves to the union type: "default" | "secondary" | "destructive" | "outline" | "applied" | "interview" | "new" | "rejected" | "offer" | null | undefined

Sweet! Now, all of our TypeScript errors should be taken care of.

Let’s keep making our data dynamic.

src/app/components/ApplicationsTable.tsx
<TableCell>
April 15, 2025
{application.dateApplied?.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</TableCell>

For the date, we can use the application.dateApplied property. Then, we can format the date using the toLocaleDateString JavaScript method. For more options, see MDN's Date.prototype.toLocaleDateString documentation.

For the job title:

src/app/components/ApplicationsTable.tsx
<TableCell>
Software Engineer
{application.jobTitle}
</TableCell>

For the company name:

src/app/components/ApplicationsTable.tsx
<TableCell>
RedwoodJS
{application.company.name}
</TableCell>

For the contact name, we’re just going to reference the first name within our contacts array.

src/app/components/ApplicationsTable.tsx
<TableCell className="flex items-center gap-2">
<Avatar>
<AvatarFallback>J</AvatarFallback>
<AvatarFallback>{application.company.contacts[0].firstName.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
John Doe
{application.company.contacts[0].firstName} {application.company.contacts[0].lastName}
</TableCell>

For the salary range:

src/app/components/ApplicationsTable.tsx
<TableCell>
$150,000-$250,000
${application.salaryMin}-${application.salaryMax}
</TableCell>

For the view link:

src/app/components/ApplicationsTable.tsx
<TableCell>
<a href="#">
<a href={link("/applications/:id", { id: application.id })}>
<Icon id="view" />
</a>
</TableCell>

Be sure to import link at the top of the file:

src/app/components/ApplicationsTable.tsx
import { link } from "../shared/links";

You’ll probably see another error. We need to do a little more setup to make this work.

In our worker.tsx file, let’s add our route:

src/app/worker.tsx
prefix("/applications", [
route("/", [isAuthenticated, List]),
route("/:id", [isAuthenticated, () => <h1>Application</h1>]),
]),
  • We can nest our route definition within the applications prefix.
  • The : designates that our route is dynamic and contains the id in the URL.
  • We included isAuthenticated to ensure that the route is protected.
  • Temporarily, we'll display an <h1> Application heading.

Now, let’s jump over to shared/links.ts file and add /applications/:id to our array:

src/app/shared/links.ts
export const link = defineLinks([
7 collapsed lines
"/",
"/user/login",
"/user/signup",
"/user/logout",
"/legal/privacy",
"/legal/terms",
"/applications",
"/applications/:id",
]);

Perfect. Everything should check out. Try clicking on the view icon in the browser. You should be redirected to a page with an “Application” heading.

Final ApplicationsTable.tsx file
src/app/components/ApplicationsTable.tsx
import { Application, ApplicationStatus, Company, Contact } from "@prisma/client"
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"
import { Avatar, AvatarFallback } from "./ui/avatar"
import { Badge } from "./ui/badge"
import { Icon } from "./Icon"
import type { badgeVariants } from "./ui/badge"
import { VariantProps } from "class-variance-authority"
import { link } from "../shared/links"
const ApplicationsTable = ({ applications }: {
applications: (Application & {
applicationStatus: ApplicationStatus,
company: Company & {
contacts: Contact[]
},
})[]
}) => {
return (
<Table>
<TableHeader>
<TableHead className="w-[100px]">Status</TableHead>
<TableHead>Date Applied</TableHead>
<TableHead>Job Title</TableHead>
<TableHead>Company</TableHead>
<TableHead>Contact</TableHead>
<TableHead>Salary Range</TableHead>
<TableHead></TableHead>
</TableHeader>
<TableBody>
{applications.map(application => (
<TableRow>
<TableCell>
<Badge variant={application.applicationStatus?.status.toLowerCase() as VariantProps<typeof badgeVariants>['variant']}>
{application.applicationStatus?.status}
</Badge>
</TableCell>
<TableCell>
{application.dateApplied?.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</TableCell>
<TableCell>
{application.jobTitle}
</TableCell>
<TableCell>
{application.company.name}
</TableCell>
<TableCell className="flex items-center gap-2">
<Avatar>
<AvatarFallback>{application.company.contacts[0].firstName.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
{application.company.contacts[0].firstName} {application.company.contacts[0].lastName}
</TableCell>
<TableCell>
${application.salaryMin}-${application.salaryMax}
</TableCell>
<TableCell>
<a href={link("/applications/:id", { id: application.id })}>
<Icon id="view" />
</a>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
export { ApplicationsTable }

Finalizing the List Page

Now, let’s go back to our List.tsx and tidy up a few things.

On line 27, we can remove our JSON.stringify function:

src/app/pages/applications/List.tsx
<pre>{JSON.stringify(applications, null, 2)}</pre>

Below our table, let’s add a couple of buttons. An “Archive” button on the left and another “New Application” button on the right:

src/app/pages/applications/List.tsx
<div className="flex justify-between items-center">
<Button asChild variant="secondary">
<a href="#">
<Icon id="archive" />
Archive
</a>
</Button>
<Button asChild>
<a href="#">
<Icon id="plus" />
New Application
</a>
</Button>
</div>
  • The classes flex justify-between items-center on the wrapping div position our buttons.
  • On the first button, I've added a variant="secondary" which will make the button a light beige.
  • I also included the archive icon next to the archive text: <Icon id="archive" />
  • On the "New Application" button, I added the plus icon: <Icon id="plus" />

If you look at this within the browser, we’re getting closer:

The Archive button should link to a filtered view of all applications that have been archived. Let’s update our link:

src/app/pages/applications/List.tsx
<a href={`${link("/applications")}?status=archived`}>
<Icon id="archive" />
Archive
</a>

This is really a filtered view of our existing /applications page, so we can use the existing /applications link. Then, we’re attaching the query parameter: ?status=archived. We don’t need to do anything extra to make this link work, but we do need to adjust our List page to account for the status query parameter. We’ll come back to this!

For the New Application button, let’s update the href:

src/app/pages/applications/List.tsx
<Button asChild>
<a href={link("/applications/new")}>
<Icon id="plus" />
New Application
</a>
</Button>

/applications/new doesn’t exist yet. We need to set up this page. In our worker.tsx file, let’s add a new route:

src/app/worker.tsx
prefix("/applications", [
route("/", [isAuthenticated, List]),
route("/new", [isAuthenticated, () => <h1>New Application</h1>]),
route("/:id", [isAuthenticated, () => <h1>Application</h1>]),
]),
  • We can nest our route definition within the applications prefix.
  • We included isAuthenticated to ensure that the route is protected.
  • Temporarily, we'll display an <h1> New Application heading.

Now, let’s add our link to the links.ts file:

src/app/shared/links.ts
export const link = defineLinks([
7 collapsed lines
"/",
"/user/login",
"/user/signup",
"/user/logout",
"/legal/privacy",
"/legal/terms",
"/applications",
"/applications/new",
"/applications/:id",
]);

If we test this out within the browser, clicking on the “New Application” button at the bottom, you should see our temporary “New Application” page:

Let’s make the “New Application” button at the top, match the button at the bottom.

src/app/pages/applications/List.tsx
return (
<InteriorLayout>
<>
<div className="px-page-side flex justify-between items-center">
<h1 className="page-title">All Applications</h1>
<div>
<Button asChild>
<a href={link("/applications/new")}>
<Icon id="plus" />
New Application
</a>
</Button>
</div>
9 collapsed lines
</div>
<ApplicationsTable applications={applications} />
<div className="flex justify-between items-center">
<Button asChild variant="secondary">
<a href={`${link("/applications")}?status=archived`}>
<Icon id="archive" />
Archive
</a>
</Button>
<Button asChild>
<a href={link("/applications/new")}>
<Icon id="plus" />
New Application
</a>
</Button>
</div>
</>
</InteriorLayout>
)

Now, let’s adjust the spacing around our table. On line 22, we’re using a class of px-page-side to add some padding to the left and right side.

src/app/pages/applications/List.tsx
<div className="px-page-side flex justify-between items-center">

Let’s remove this from the div and add it to a wrapping div.

src/app/pages/applications/List.tsx
<InteriorLayout>
<div className="px-page-side">
<div className="flex justify-between items-center">
...
</div>
</div>
</InteriorLayout>
  • You'll notice, we converted the React Fragment <> to a div and added a class of px-page-side to it.
  • Then, we removed px-page-side from nested div that contains our page title.

Before we preview this in the browser, let’s adjust the vertical spacing.

src/app/pages/applications/List.tsx
<InteriorLayout>
<div className="px-page-side">
<div className="flex justify-between items-center mb-5">
10 collapsed lines
<h1 className="page-title">All Applications</h1>
<div>
<Button asChild>
<a href={link("/applications/new")}>
<Icon id="plus" />
New Application
</a>
</Button>
</div>
</div>
<div className="mb-8"><ApplicationsTable applications={applications} /></div>
<div className="flex justify-between items-center mb-10">
  • On line 22, I added mb-5 for 20px of margin on the bottom.
  • On line 33, I added mb-8 for 32px of margin on the bottom.
  • On line 44, I added mb-10 for 40px of margin on the bottom.

Now, let’s preview this in the browser:

Before we call this done, make sure that our Archive filter is working.

Using Query Params to Filter Applications

On every page we get an object that contains ctx, request, and headers. In our case, the request contains the query parameters that we’re looking for.

On List.tsx, when we define our List function, let’s accept request as a prop:

src/app/pages/applications/List.tsx
const List = async ({ request }: { request: Request }) => {

You'll notice when I type the request, I'm using a standard Request type.

Now, at the top of our function, we can get the status query parameter using the URLSearchParams method from the standard WebAPI:

src/app/pages/applications/List.tsx
const url = new URL(request.url);
const status = url.searchParams.get("status");
console.log({ status });

If you run this in the browser, you should see { status: 'archived' } displayed.

Now, that we’ve checked that our query parameter is working, we can remove our console.log statement and adjust our Prisma findMany query.

After the include object, let’s add a where object:

src/app/pages/applications/List.tsx
const applications = await db.application.findMany({
include: {
applicationStatus: true,
company: {
include: {
contacts: true
}
}
},
where: {
archived: status === "archived" ? true : false
}
});

The where clause filters the results of our query based on whether archived is true or false. We can se it to true or false depending on whether status is set to archived or not.

YOu can test this in the browser, by clicking on the Archive button. You should see the URL change to include /applications?status=archived and the entry applications table should be empty.

Let’s make this better by adding an empty state:

src/app/pages/applications/List.tsx
<div className="mb-8">
{applications.length > 0 ? (
<ApplicationsTable applications={applications} />
) : (
<div className="text-center text-sm text-muted-foreground">
No applications found
</div>
)}
</div>
  • On line 41, we're checking if the applications array has more than one item. If it does, display the ApplicationsTable.
  • If it doesn't, display a message that says "No applications were found."
  • I also added a few styles to our message.
    • Centered with text-center
    • Reduced the text size with text-sm
    • Changed the text color to text-muted-foreground

Once we click on the “Archive” button, it’s easy to feel “stuck”.

Let’s change the “Archive” button so that it toggles based on the query parameter.

src/app/pages/applications/List.tsx
<Button asChild variant="secondary">
{status === "archived" ? (
<a href={`${link("/applications")}`}>
<Icon id="archive" />
Active
</a>
) : (
<a href={`${link("/applications")}?status=archived`}>
<Icon id="archive" />
Archive
</a>
)}
</Button>
  • If the status is equal to archived then the link should go to /applications and the button label should be Active.
  • Otherwise, display the Archive button, with the query parameter ?status=archived

Test it out in the browser:

It looks and works great!

Remember, you can use Prisma Studio to add more seed data, if you want to test various states.

Final code for List.tsx
src/app/pages/applications/List.tsx
import { InteriorLayout } from "@/app/layouts/InteriorLayout";
import { db } from "@/db";
import { Button } from "@/app/components/ui/button";
import { ApplicationsTable } from "@/app/components/ApplicationsTable";
import { Icon } from "@/app/components/Icon";
import { link } from "@/app/shared/links";
const List = async ({ request }: { request: Request }) => {
const url = new URL(request.url);
const status = url.searchParams.get("status");
const applications = await db.application.findMany({
include: {
applicationStatus: true,
company: {
include: {
contacts: true
}
}
},
where: {
archived: status === "archived" ? true : false
}
});
return (
<InteriorLayout>
<div className="px-page-side">
<div className="flex justify-between items-center mb-5">
<h1 className="page-title">All Applications</h1>
<div>
<Button asChild>
<a href={link("/applications/new")}>
<Icon id="plus" />
New Application
</a>
</Button>
</div>
</div>
<div className="mb-8">
{applications.length > 0 ? (
<ApplicationsTable applications={applications} />
) : (
<div className="text-center text-sm text-muted-foreground">
No applications found
</div>
)}
</div>
<div className="flex justify-between items-center mb-10">
<Button asChild variant="secondary">
{status === "archived" ? (
<a href={`${link("/applications")}`}>
<Icon id="archive" />
Active
</a>
) : (
<a href={`${link("/applications")}?status=archived`}>
<Icon id="archive" />
Archive
</a>
)}
</Button>
<Button asChild>
<a href={link("/applications/new")}>
<Icon id="plus" />
New Application
</a>
</Button>
</div>
</div>
</InteriorLayout>
)
}
export { List }