DEV Community

Cover image for Building the Basic UI of my SaaS Product - Update 4
Andrew Jones
Andrew Jones

Posted on

Building the Basic UI of my SaaS Product - Update 4

Back after a long hiatus! In this series, I'm building a Software-as-a-Service product for syncing environment variables between team members, and with CI/CD pipelines. Since my background is mostly frontend, I'll be learning about backend technologies on the way. You can read more about my concept and technology selection in the first post, project setup in the second post, and adding authentication in the third post. I also wrote this tutorial based on some of what I learned about database setup.

In this post, I'll build out the UI and functionality for viewing the organizations and projects associated with a user, the UI that leads to the core functionality of saving variables.

Approach

Generally as a rule-of-thumb, people say not to spend too much time on the frontend until the core backend functionality is worked out. Since I'm a frontend dev and I didn't want to burn out working through backend work, I decided to work on them both. Sure, I might need to change some of the frontend work if the backend functionality needs changes, but I'll cross that bridge if it happens. More important to keep myself engaged. This approach also helps me figure out what backend APIs I need to implement!

Layout UI

I looked at other dev tool sites for inspiration and landed on a tab menu similar to Vercel's dashboard. I created a Layout component and wrapped all pages with it in my pages/_app.tsx file.

When a user is signed in, the Layout component will show their profile picture in round format. The GitHub auth provider provides the URL for this picture when a user registers.

{session && (
    <div className="float-right h-10 rounded-full flex">
      <span className="flex-1 pt-2.5 mr-5">{session.user?.name}</span>
      <img
        src={session.user!.image!}
        alt="Your Profile Picture"
        className="h-12 rounded-full border-purple-100 border-solid border-2"
      />
    </div>
)}
Enter fullscreen mode Exit fullscreen mode

I also created a NavLink tab component and used it to create the tab menu based on the tabs I imagine the site will need. And finally, I picked a nice dark purple for the background. I may add a light-mode later with the purple used for the text instead of the background.

Tab menu

User Creation Functionality

In the last post I added sign-in and registration through GitHub. I was able to get an account set up and saved in my DB, but there was a problem: this account had no organizations or projects. While I might change this later based on the pricing model, I decided to add a personal organization and starter project to new users (similar to the GitHub personal organization).

Next-Auth supports a custom callback when a user is created, so in my Next-Auth config in pages/auth/[...nextauth].ts file, I added this event:

events: {
    createUser: async (user) => {
      await prisma.organization.create({ //create a new organization in the DB
        data: {
          name: 'My Organization',
          adminUsers: { // create a new adminUser-organization relation using this org and the given userId
            create: {
              userId: String(user.id),
            },
          },
          projects: {
            create: { // Create a new project so the user has something to start with, and link it to this org
              name: 'My First Project',
              ...
            }
          },
          ...
        }
      })
    }
}
Enter fullscreen mode Exit fullscreen mode

Using psql to interact with my database, I deleted my user, and then re-created it using the frontend to test this code. I verified that an organization and project were created and linked to the new user.

Organization Pages and API Routes

Next, I needed a way to fetch the signed-in user's organizations so that the frontend could show a list of their organizations. I added this simple API route which gets the session using Next-Auth, and then searches the database for every organization where the user associated with this session is an admin:

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const userSession = await getSession({ req });
  const orgs = await prisma.organization.findMany({
    where: {
      adminUsers: {
        every: {
          userId: userSession!.user!.id
        }
      }
    },
    include: {
      projects: true
    }
  })
  res.status(200).json({ myOrganizations: orgs })
}
Enter fullscreen mode Exit fullscreen mode

Since I have cookies saved on localhost:3000 in my browser for the sign-in, I can test this API route by navigating to it directly in my browser:
List of my organizations in JSON format

I also tested from an incognito window where I'm not signed in, and as expected, got an internal server error. Why? Because I used this TypeScript non-null assertion (!) here userId: userSession!.user!.id which is really a lie to the compiler - userSession CAN be null, when someone tries to access this endpoint without signing in. So instead of !, we need to add a check for the userSession to not be null and return an error if it is. I added this check above the findMany call:

  if(!userSession?.user){
    res.status(401).json({
      error: {
        code: 'no-access',
        message: 'You need to sign-in to view your organizations.',
      },
    })
    return
  }
Enter fullscreen mode Exit fullscreen mode

Finally, I added a frontend page pages/organizations/index.ts which very simply fetches data from /api/organizations/my-organizations and then displays loading, the data, or any errors based on the page state:

import { useEffect, useState } from 'react';
import { OrgCard } from '@components/OrgCard'
import { OrganizationWithProjects } from '@project-types/organization';

export default function ListMyOrganizations() {
  const [myOrgs, setMyOrgs] = useState<OrganizationWithProjects[]>();
  const [error, setError] = useState();

  useEffect(() => {
    fetch('/api/organizations/my-organizations')
      .then((res) => res.json())
      .then(orgsData => setMyOrgs(orgsData.myOrganizations))
      .catch(setError);
  }, []);

  if(!myOrgs && !error) return <div>Loading...</div>
  else if(error) return <div>{JSON.stringify(error)}</div>
  else return (
    <div>
      {myOrgs!.map((org: OrganizationWithProjects) => <OrgCard organization={org} key={org.id}/>)}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The result looks like this:
Organizations list

The Organization Page / Project List

I won't get into too much detail here, but I followed roughly the same process to design and implement the Organization "Profile" page. Keeping it simple for now, there's a list of projects and their environments, a free-trial marker, and a list of admins with an "Add" button (not functional yet). I'll probably add a few more "design" elements like borders and colors later.

Organization profile page described above

Conclusion

In this post, we added the basic UI which takes users to the Project page. In the next article, we'll finally implement the core functionality of the site: the project page and secure variable storage.

Follow me here or on Twitter for more updates and other content. Feel free to DM me for questions!

Latest comments (0)