Our great sponsors
-
taxonomy
An open source application built using the new router, server components and everything new in Next.js 13.
-
SurveyJS
Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App. With SurveyJS form UI libraries, you can build and style forms in a fully-integrated drag & drop form builder, render them in your JS app, and store form submission data in any backend, inc. PHP, ASP.NET Core, and Node.js.
// app/(main)/profile/new/page.tsx "use client"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { useFieldArray, useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { newProfileSchema } from "@/lib/validators/newProfile"; import { TrashIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; export type NewProfileInput = z.infer; export default function NewProfileForm() { const form = useForm({ resolver: zodResolver(newProfileSchema), defaultValues: { firstName: "", lastName: "", role: undefined, skills: [{ name: "" }], bio: "", github: "", linkedin: "", website: "", }, }); const { fields, append, remove } = useFieldArray({ control: form.control, name: "skills", }); const watchSkills = form.watch("skills"); const router = useRouter(); const onSubmit = async (data: NewProfileInput) => { console.log(data); }; return ( Create Profile Yabba dabba doo ( First name )} /> ( Last name )} /> ( Role Full Stack Frontend Backend Design )} /> ( Skills {fields.map((field, index) => ( {fields.length > 1 && ( remove(index)} /> )} {form.formState.errors.skills?.[index]?.name && ( This can't be empty )} ))} !field.name)} onClick={() => append({ name: "" })} className="max-w-min" > Add Skill )} /> ( Bio )} />
( Github Username )} /> ( Linkedin Username )} />( Website )} /> Submit ); }Enter fullscreen mode Exit fullscreen modeContext and Private Procedure
Now we have the form but we need to push this data to the database when the user submits it. To do that we need to create a new trpc procedure. Before we actually write the procedure there are a few things we need to add.
The first step is to add the userId to the trpc context. This will make things a lot quicker because it means we don't have to pass the userId from the client. For example if we want to get the current user's posts, instead of passing the current user id to the server, we can just call the function with no props and it already knows who the user is.
Navigate to the
server/api/trpc.ts
file and import the supabase client:
// server/api/trpc.ts ... import { createClient } from "@/utils/supabase/server"; import { cookies } from "next/headers"; ...
Enter fullscreen mode Exit fullscreen modeNow inside the
createTRPCContext
function we need to pass the userId to the context like this:
// server/api/trpc.ts ... export const createTRPCContext = async (opts: { headers: Headers }) => { const supabase = createClient(cookies()); const { data: { user }, } = await supabase.auth.getUser(); return { user, db, ...opts, }; }; ...
Enter fullscreen mode Exit fullscreen modeThe next step is to create a new type of procedure called private procedure, which is a procedure that can only be called by authenticated users. This is helpful because it means that we know there is a userId for our queries. If we were to use a public procedure we would have to assert that we know the user is authenticated and handle that on the client which is a lot more complicated. To do this add the following code at the bottom of the file:
// server/api/trpc.ts ... const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { if (!ctx.user) { throw new TRPCError({ code: "UNAUTHORIZED", }); } return next({ ctx: { user: ctx.user, }, }); }); export const privateProcedure = t.procedure.use(enforceUserIsAuthed); ...
Enter fullscreen mode Exit fullscreen modeProcedure
Now we can get started writing the procedure. Go to
server/api/routers
and you should see aposts.ts
file. Delete that file and create a new one calledprofiles.ts
. Inside that file add the following code:
// server/api/routers/profiles.ts import { z } from "zod"; import { createTRPCRouter, privateProcedure } from "@/server/api/trpc"; import { RoleType } from "@prisma/client"; export const profileRouter = createTRPCRouter({ create: privateProcedure .input( z.object({ firstName: z.string(), lastName: z.string(), role: z.nativeEnum(RoleType), skills: z.array(z.string()), bio: z.string(), github: z.string(), linkedin: z.string(), website: z.union([z.literal(""), z.string().trim().url()]), }), ) .mutation(async ({ ctx, input }) => { const user = await ctx.db.profile.create({ data: { id: ctx.user.id, email: ctx.user.email!, firstName: input.firstName, lastName: input.lastName, role: input.role, skills: input.skills, bio: input.bio, github: input.github, linkedin: input.linkedin, website: input.website, }, }); return user; }), });
Enter fullscreen mode Exit fullscreen modeNow we need to expose this router to the client. Go to
server/api/root.ts
and add the following code:
// server/api/root.ts import { profileRouter } from "@/server/api/routers/profiles"; import { createTRPCRouter } from "@/server/api/trpc"; export const appRouter = createTRPCRouter({ profiles: profileRouter, }); export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen modeNow we need to call the mutation on submit on form submission. Before we do that though, lets create a utility function to capitalize just the first letter of the string, so all of the names are uniform. In
lib/utils.ts
add the following function:
// lib/utils.ts ... export function capitalizeFirstLetter(inputString: string) { const lowercaseString = inputString.toLowerCase(); const capitalizedString = lowercaseString.charAt(0).toUpperCase() + lowercaseString.slice(1); return capitalizedString; } ...
Enter fullscreen mode Exit fullscreen modeNow we can use our mutation. Add the following code to
app/(main)/profile/new/page.tsx
:
// app/(main)/profile/new/page.tsx const router = useRouter(); const { mutate } = api.profiles.create.useMutation({ onSuccess: () => { router.push("/profile"); }, onError: (e) => { const errorMessage = e.data?.zodError?.fieldErrors.content; console.error("Error creating investment:", errorMessage); }, }); const onSubmit = async (data: NewProfileInput) => { const skillsList = data.skills.map((skill) => skill.name); mutate({ firstName: capitalizeFirstLetter(data.firstName), lastName: capitalizeFirstLetter(data.lastName), role: data.role, skills: skillsList, bio: data.bio, github: data.github, linkedin: data.linkedin, website: data.website, }); };
Enter fullscreen mode Exit fullscreen modeNow when you submit the form you should be redirected to a profile page and when you check the table in supabase the new data should be there. Now that we have the data, let's make it so we can view it.
Querying data
Go back to
server/api/routers/profiles.ts
and add the following query below your mutation:
// server/api/routers/profiles.ts ... getCurrent: privateProcedure.query(async ({ ctx }) => { const profile = await ctx.db.profile.findUnique({ where: { id: ctx.user.id! }, }); return profile; }), ...
Enter fullscreen mode Exit fullscreen modeCreate a new page in the profile folder and add the following code:
// app/(main)/profile/page.tsx import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, } from "@/components/ui/card"; import { capitalizeFirstLetter } from "@/lib/utils"; import { api } from "@/trpc/server"; export default async function ProfilePage() { const profile = await api.profiles.getCurrent.query(); if (!profile) return null; return (
{profile.firstName} {profile.firstName} {profile.email}); }Role: {capitalizeFirstLetter(profile.role)}
Skills:
{profile.skills.map((skill, index) => ({skill}
))}Bio: {profile.bio}
Github: {profile.github}
Linkedin: {profile.linkedin}
Website: {profile.website}
Enter fullscreen mode Exit fullscreen modeThis isn't styled very well, its just supposed to show how you can fetch the data on the server side with trpc. Notice that we are importing the api from the server folder not the react folder. This means we cant use hooks like usequery and usemutation because we are in a server component.
Conclusion
This app doesn't really do anything, it is just supposed to get you started where everything is configured properly to design a cool web application. You can add to your prisma schema and start doing more complicated queries and mutations with the same principles used for just a simple profile.
All of the code for this project will be at this repo.
Thanks for reading!
P.S. If you are a software developer in college shoot me an email at [email protected]. I am going to start building a website for college students to find other developers in their area to build projects together. If you want to help me build it or use it or provide feedback or whatever I would love to chat.
I am building this app with inspiration from Taxonomy and Acme corp so a lot of the design comes from there.