iTranslated by AI
I Built a Poll Service with Comment Functionality
Introduction
There are many issues in the world that must be settled.
- Kinoko or Takenoko (Mushroom or Bamboo shoot)
- Windows or Mac
- Android or iPhone
And so on...
I have created a place to serve as the main battlefield for such debates.
Features
- Creating polls
- Voting in polls
- Commenting on polls you've voted in
*Login is required for all of these

Technologies Used
Stack
- Hosting: Vercel
- DB: PlanetScale
- Domain: Cloudflare
Frameworks, etc.
- NextJS (App Router)
- NextAuth
- Prisma
- react-hook-form
- MUI
Things I Used for the First Time
PlanetScale
It's really great.
Can I really use 5GB? I can't even use it all.
I haven't made any adjustments to save on reads yet, so I'll monitor the situation and adjust accordingly in the future.
Prisma
Since it was almost my first time using an RDB, I'm not sure which would have been better: learning SQL or using Prisma. When updating schema.prisma, you need to run prisma generate, etc. I didn't realize it wouldn't work properly while debugging was running and that I had to stop it before running prisma generate, so I lost quite a bit of time here.
It's really great that the retrieved data comes with types!

react-hook-form
It's really great.
At first, it looked a bit confusing, but once I used it, it was simple.
Basically, if you just throw it into the render of the Controller and pass the field like {...field}, it works like a charm.
<Controller
name="commentText"
control={control}
rules={validationRules.commentText}
render={({ field, fieldState }) => (
<TextField
{...field}
placeholder="コメント"
fullWidth
multiline
error={fieldState.invalid}
helperText={fieldState.error?.message}
/>
)}
/>
Validation can also be defined as follows and loaded into the rules of the Controller, and it will handle it automatically.
const validationRules = {
commentText: {
required: "コメントを入力してください",
minLength: { value: 1, message: "1文字以上入力してください" },
},
};
The execution status during form submission can also be easily retrieved with formState.isSubmitting!
const { control, handleSubmit, formState } = useForm<Inputs>();
...
<LoadingButton
type="submit"
variant="contained"
color="secondary"
loading={formState.isSubmitting}
disableElevation
sx={{ fontWeight: "bold" }}
>
作成
</LoadingButton>
...
Next.js Features
Server Actions
I implemented the application primarily with SSR using App Router and utilized Server Actions for data fetching as well. Only the posting form components are running on the client side. It was very easy since it can be done without an API.
const onSubmit: SubmitHandler<Inputs> = async (data: Inputs) => {
const result = await addComment(data); // This is executed on the server side
if ("error" in result) {
// Error handling
} else {
// Success handling
}
};
By writing "use server", it runs on the server side.
"use server";
import { prisma } from "@/db/db";
export async function addComment(props: {
pollId: number;
commentText: string;
}) {
const { pollId, commentText } = props;
try {
const comment = await prisma.comment.create({
...
});
return comment;
} catch (error) {
return { error: error };
}
}
Parallel Routes
The screen is conditionally rendered based on the user's status: a voting screen if they haven't voted, and a result screen if they have already voted.
detail/[id]
├── @form
├── @guest
├── @result
├── @voted
└── layout.tsx
The layout.tsx handles the conditional rendering as follows.
export default async function Layout({
form,
voted,
guest,
result,
params,
}: {
form: React.ReactNode;
voted: React.ReactNode;
guest: React.ReactNode;
result: React.ReactNode;
params: { id: string };
}) {
const id = Number(params.id);
if (isNaN(id)) {
notFound();
}
const session = await getServerSession(options);
const poll = await getPoll(id);
const isClosed = poll.closed || (poll.deadline && poll.deadline < new Date());
// If the poll is closed, show the result screen
if (isClosed) {
return <section>{result}</section>;
}
// If not logged in, show the guest screen
if (!session) {
return <section>{guest}</section>;
}
// If already voted, show the "voted" screen; otherwise, show the form screen
const IsVoted = poll.votes.some((vote) => vote.authorId === session.user.uid);
return <section>{IsVoted ? voted : form}</section>;
}
NextAuth
It's really great.
I won't write about the general usage here since it's explained clearly on the official site and other articles.
I'll introduce a few points that were a bit hard to understand.
Adding Information
Here is an example of adding role information.
First, declare the types somewhere.
import type { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
role: string;
} & DefaultSession["user"];
}
}
declare module "next-auth/jwt" {
interface JWT {
role: string;
}
}
Next, add the logic to store the information during the initial login.
import type { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const options: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
session: { strategy: "jwt" },
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
jwt: async ({ token, account }) => {
if (account) {
// Logic for the first login
// Add role to the token
if (token.email == 'example.com') {
token.role = 'admin';
} else {
token.role = 'user';
}
}
return token;
},
session: async ({ session, token }) => {
// If using session, also add role to the session
session.user.role = token.role;
return session;
},
},
};
With this, you can handle it in middleware or retrieve information from the session.
export default async function Layout({
children
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(options);
const role = session.role;
return <section>{children}</section>
}
Middleware Processing
By writing middleware logic, you can specify pages that require authentication or add processing during authentication.
The middleware should be created directly under src.
If you only need to specify pages that require authentication, you can just do the following:
export { default } from "next-auth/middleware"
export const config = {
matcher: [
"/user/:path*", // If you want all pages under /user to require authentication
"/dashboard" // Only /dashboard will require authentication. *Paths under /dashboard will not be authenticated
]
}
If you want to perform checks, such as whether the user has permission to access the page during authentication, do the following:
When a path specified in the matcher in config is accessed, callbacks.authorized is executed. If it returns true, access is granted; if false, it redirects to the path specified in signIn.
import { withAuth } from "next-auth/middleware";
export default withAuth(
// function middleware(req) {
// // Executed if callbacks.authorized returns true
// },
{
callbacks: {
// Executed when the path matches the one specified in config.matcher
// Return true to allow access, false to deny access
authorized: ({ req, token }) => {
if (req.nextUrl.pathname.startsWith("/admin")) {
// If accessing a path under /admin, return true (allow access) only if the role stored in token.role is 'admin'
const roleIsAdmin = token.role === 'admin'
return roleIsAdmin;
}
// To allow access only when authenticated.
// Without this, access to pages under /user specified in config would not be possible.
return !!token;
},
},
pages: {
// If callbacks.authorized returns false, the user is redirected to the path specified here
signIn: "/",
},
}
);
export const config = {
matcher: ["/admin/:path*","/user/:path*"],
};
Future Plans
- I haven't implemented pagination yet, so I want to add it (it currently can only display up to 50 items).
- Some parts of the UI are a bit rough, so I want to improve them. As you can probably tell, I implemented it while looking at how Zenn and Reddit do things without overthinking it too much.
- There are some areas where the UI reflects changes slowly, so I want to fix that. I'd like to use
useOptimisticproperly. - Promotion (it seems like the most important thing, but it's also the one I understand the least).
Conclusion
I came up with the idea on a whim and implemented it in about half a month while watching One Piece. Although it's a bit unpolished in some areas, I've implemented the bare minimum and released it. (I'm sure I'm not the only one who gets a lot of work done while watching One Piece.)
Speed is really important in solo development, isn't it?
If people find it useful, I intend to keep going!
References
For react-hook-form and Server Actions, the videos on this channel were very easy to understand and extremely helpful!
Discussion