chai or code -> next.js
-> anonymous feedback -> code --> video
basic home page
login (admin)
register/signup (new user)--> username,email,password
- then send you the email to verify your acc --> 6 digit code
verification --> code
login/signin -->email,password --> custom nextAuth
then only can access dashboard
dashboard
url and copy button to copy url
accept message -> on/off
on -> accept anonymous feedback
off -> reject anonymous feedback
get feedback(refresh)
all feedback visible
delete feedback
url (no login req)-> open this url to give feedback
u can type feedback and send
u can use ai suggestion to send feedback
-
Mastering ZOD for validation in Nextjs
npx create-next-app@latest
# name
# typescript -> yes
# tailwind css -> yes
# src directory -> yes
# App router -> yes
# import alias -> no
npm run dev
mongo db schema
// src/model/User.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface Message extends Document {
content: string;
createdAt: Date;
}
// Updated Message schema
const MessageSchema: Schema<Message> = new mongoose.Schema({
content: {
type: String,
required: true,
},
createdAt: {
type: Date,
required: true,
default: Date.now,
},
});
export interface User extends Document {
username: string;
email: string;
password: string;
verifyCode: string;
verifyCodeExpiry: Date;
isVerified: boolean;
isAcceptingMessages: boolean;
messages: Message[];
}
// Updated User schema
const UserSchema: Schema<User> = new mongoose.Schema({
username: {
type: String,
required: [true, 'Username is required'],
trim: true,
unique: true,
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
match: [/.+\@.+\..+/, 'Please use a valid email address'],
},
password: {
type: String,
required: [true, 'Password is required'],
},
verifyCode: {
type: String,
required: [true, 'Verify Code is required'],
},
verifyCodeExpiry: {
type: Date,
required: [true, 'Verify Code Expiry is required'],
},
isVerified: {
type: Boolean,
default: false,
},
isAcceptingMessages: {
type: Boolean,
default: true,
},
messages: [MessageSchema],
});
// already present then return that model schema || if 1st time then create schema and then return
const UserModel =
(mongoose.models.User as mongoose.Model<User>) ||
mongoose.model<User>('User', UserSchema);
export default UserModel;
npm i zod
zod schema validation (for each component, where user input is taken)->manual validation
//src/schemas/signUpSchema.ts
import { z } from 'zod';
export const usernameValidation = z
.string()
.min(2, 'Username must be at least 2 characters')
.max(20, 'Username must be no more than 20 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Username must not contain special characters');
export const signUpSchema = z.object({
username: usernameValidation,
email: z.string().email({ message: 'Invalid email address' }),
password: z
.string()
.min(6, { message: 'Password must be at least 6 characters' }),
});
//src/schemas/verifySchema.ts
import { z } from 'zod';
export const verifySchema = z.object({
code: z.string().length(6, 'Verification code must be 6 digits'),
});
//src/schemas/signInSchema.ts
import { z } from 'zod'
export const signInSchema = z.object({
identifier: z.string(),
password: z.string(),
});
//src/schemas/messageSchema.ts
import { z } from 'zod'
export const messageSchema = z.object({
content: z
.string()
.min(10, { message: 'Content must be at least 10 characters.' })
.max(300, { message: 'Content must not be longer than 300 characters.' }),
});
//src/schemas/AcceptMessageSchema.ts
import { z } from 'zod'
export const AcceptMessageSchema = z.object({
acceptMessages: z.boolean(),
});
How to connect Database in NextJS
Edge time framework (only run back-end when it is required)
if db connection instance is
present -> then use that instance , dont create multiple db instance
not present -> create db instance and use it
//src/lib/dbConnect.ts
import mongoose from 'mongoose';
type ConnectionObject = {
isConnected?: number;
};
const connection: ConnectionObject = {};
async function dbConnect(): Promise<void> {
// Check if we have a connection to the database or if it's currently connecting
if (connection.isConnected) {
console.log('Already connected to the database');
return;
}
try {
// Attempt to connect to the database
const db = await mongoose.connect(process.env.MONGODB_URI || '', {});
connection.isConnected = db.connections[0].readyState;
console.log('Database connected successfully');
} catch (error) {
console.error('Database connection failed:', error);
// Graceful exit in case of a connection error
process.exit(1);
}
}
export default dbConnect;
Setup Resend email with NextJS
to send verification email while signup/register(1st time user)
resend --> to send email
react-email --> to create actual data to be written on email, and verify link to verify account
npm i resend
//src/lib/resend.ts
import { Resend } from 'resend';
export const resend = new Resend(process.env.RESEND_API_KEY);
np i react-email
//email/VerificationEmail.tsx
import {
Html,
Head,
Font,
Preview,
Heading,
Row,
Section,
Text,
Button,
} from '@react-email/components';
interface VerificationEmailProps {
username: string;
otp: string;
}
export default function VerificationEmail({ username, otp }: VerificationEmailProps) {
return (
<Html lang="en" dir="ltr">
<Head>
<title>Verification Code</title>
<Font
fontFamily="Roboto"
fallbackFontFamily="Verdana"
webFont={{
url: 'https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2',
format: 'woff2',
}}
fontWeight={400}
fontStyle="normal"
/>
</Head>
<Preview>Here's your verification code: {otp}</Preview>
<Section>
<Row>
<Heading as="h2">Hello {username},</Heading>
</Row>
<Row>
<Text>
Thank you for registering. Please use the following verification
code to complete your registration:
</Text>
</Row>
<Row>
<Text>{otp}</Text>
</Row>
<Row>
<Text>
If you did not request this code, please ignore this email.
</Text>
</Row>
{/* <Row>
<Button
href={`http://localhost:3000/verify/${username}`}
style={{ color: '#61dafb' }}
>
Verify here
</Button>
</Row> */}
</Section>
</Html>
);
}
//src/types/ApiResponse.ts
// common for all response
import { Message } from "@/model/User";
export interface ApiResponse {
success: boolean;
message: string;
isAcceptingMessages?: boolean;
messages?: Array<Message>
};
// src/helpers/sendVerificationEmail.ts
import { resend } from "@/lib/resend";
import VerificationEmail from "../../emails/VerificationEmail";
import { ApiResponse } from '@/types/ApiResponse';
export async function sendVerificationEmail(
email: string,
username: string,
verifyCode: string
): Promise<ApiResponse> {
try {
await resend.emails.send({
from: 'dev@hiteshchoudhary.com',
to: email,
subject: 'Mystery Message Verification Code',
react: VerificationEmail({ username, otp: verifyCode }),
});
return { success: true, message: 'Verification email sent successfully.' };
} catch (emailError) {
console.error('Error sending verification email:', emailError);
return { success: false, message: 'Failed to send verification email.' };
}
}
api to send this email
npm i bcryptjs
Signup user and custom OTP in NextJS
// src/app/api/sign-up/route.ts
import dbConnect from '@/lib/dbConnect';
import UserModel from '@/model/User';
import bcrypt from 'bcryptjs';
import { sendVerificationEmail } from '@/helpers/sendVerificationEmail';
export async function POST(request: Request) {
await dbConnect();
try {
const { username, email, password } = await request.json();
const existingVerifiedUserByUsername = await UserModel.findOne({
username,
isVerified: true,
});
if (existingVerifiedUserByUsername) {
return Response.json(
{
success: false,
message: 'Username is already taken',
},
{ status: 400 }
);
}
const existingUserByEmail = await UserModel.findOne({ email });
let verifyCode = Math.floor(100000 + Math.random() * 900000).toString();
if (existingUserByEmail) {
if (existingUserByEmail.isVerified) {
return Response.json(
{
success: false,
message: 'User already exists with this email',
},
{ status: 400 }
);
} else {
const hashedPassword = await bcrypt.hash(password, 10);
existingUserByEmail.password = hashedPassword;
existingUserByEmail.verifyCode = verifyCode;
existingUserByEmail.verifyCodeExpiry = new Date(Date.now() + 3600000);
await existingUserByEmail.save();
}
} else {
const hashedPassword = await bcrypt.hash(password, 10);
const expiryDate = new Date();
expiryDate.setHours(expiryDate.getHours() + 1);
const newUser = new UserModel({
username,
email,
password: hashedPassword,
verifyCode,
verifyCodeExpiry: expiryDate,
isVerified: false,
isAcceptingMessages: true,
messages: [],
});
await newUser.save();
}
// Send verification email
const emailResponse = await sendVerificationEmail(
email,
username,
verifyCode
);
if (!emailResponse.success) {
return Response.json(
{
success: false,
message: emailResponse.message,
},
{ status: 500 }
);
}
return Response.json(
{
success: true,
message: 'User registered successfully. Please verify your account.',
},
{ status: 201 }
);
} catch (error) {
console.error('Error registering user:', error);
return Response.json(
{
success: false,
message: 'Error registering user',
},
{ status: 500 }
);
}
}
Crash course on Next Auth or Authjs
providers
Oauth --> github,google,facebook --> 2 attribute -> clientId, clientSecret
Email
credentials
callback
signin
redirect
session
jwt
custom pages
// src/app/api/auth/[...nextauth]/options.ts
// creating signin form page by nextAuth --> email,password
import { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import dbConnect from '@/lib/dbConnect';
import UserModel from '@/model/User';
// to access email,username and password
//credentials.identifier.email
//credentials.identifier.username
//credentials.password
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
id: 'credentials',
name: 'Credentials',
credentials: { // form input
email: { label: 'Email', type: 'text' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials: any): Promise<any> { // return user to providers, when password compared between user and credential
await dbConnect();
try {
const user = await UserModel.findOne({
$or: [
{ email: credentials.identifier },
{ username: credentials.identifier },
],
});
if (!user) {
throw new Error('No user found with this email');
}
if (!user.isVerified) {
throw new Error('Please verify your account before logging in');
}
const isPasswordCorrect = await bcrypt.compare(
credentials.password,
user.password
);
if (isPasswordCorrect) {
return user;
} else {
throw new Error('Incorrect password');
}
} catch (err: any) {
throw new Error(err);
}
},
}),
],
// all data from user is dumped to token, -> token is dumped to to session
// this help to prevent unnecessary call to db, if i get token or session i can access any value
// inbuild interface is modified due to type error in user and token during assining is able to be fixed with following below file
callbacks: {
// return user to providers, can be access here
async jwt({ token, user }) {
if (user) {
// user.---- -->will give type error, since we dont declared type to user
token._id = user._id?.toString(); // Convert ObjectId to string
token.isVerified = user.isVerified;
token.isAcceptingMessages = user.isAcceptingMessages;
token.username = user.username;
}
return token;
},
async session({ session, token }) {
if (token) {
// token.---- -->will give type error, since we dont declared type to token
session.user._id = token._id;
session.user.isVerified = token.isVerified;
session.user.isAcceptingMessages = token.isAcceptingMessages;
session.user.username = token.username;
}
return session;
},
},
session: {
strategy: 'jwt',
},
secret: process.env.NEXTAUTH_SECRET,
// modified page route url
pages: {
signIn: '/sign-in',
},
};
// src/types/next-auth.d.ts
// modify alredy declared interface
import 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
_id?: string;
isVerified?: boolean;
isAcceptingMessages?: boolean;
username?: string;
} & DefaultSession['user'];
}
interface User {
_id?: string;
isVerified?: boolean;
isAcceptingMessages?: boolean;
username?: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
_id?: string;
isVerified?: boolean;
isAcceptingMessages?: boolean;
username?: string;
}
}
// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth/next';
import { authOptions } from './options';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
// src/middleware.ts
// when page loads then only middleware will run
// it give access to all url if token is present otherwise redirect to signin page
import { NextRequest, NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
export { default } from 'next-auth/middleware';
export const config = {
matcher: ['/dashboard/:path*', '/sign-in', '/sign-up', '/', '/verify/:path*'],
};
export async function middleware(request: NextRequest) {
const token = await getToken({ req: request });
const url = request.nextUrl;
// Redirect to dashboard if the user is already authenticated
// and trying to access sign-in, sign-up, or home page
if (
token &&
(url.pathname.startsWith('/sign-in') ||
url.pathname.startsWith('/sign-up') ||
url.pathname.startsWith('/verify') ||
url.pathname === '/')
) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
if (!token && url.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
return NextResponse.next();
}
OTP verification and unique username check in Nextjs
//src/app/api/check-username-unique/route.ts
import dbConnect from '@/lib/dbConnect';
import UserModel from '@/model/User';
import { z } from 'zod';
import { usernameValidation } from '@/schemas/signUpSchema';
// localhost:3000/api/check-username-unique?username='gokul'?isverified='true'
const UsernameQuerySchema = z.object({
username: usernameValidation,
});
export async function GET(request: Request) {
await dbConnect();
try {
// to reterive --> username='gokul' from --> localhost:3000/?username='gokul'?isverified='true'
const { searchParams } = new URL(request.url);
const queryParams = {
username: searchParams.get('username'),
};
// to check username in query,satisfy the condition username in zod schema
// i.e gokul satisfy all condition like min, max length ...
const result = UsernameQuerySchema.safeParse(queryParams);
// result.success
// result.error.format().username?._errors --> only take out username?._errors (for this scenerio)
if (!result.success) {
const usernameErrors = result.error.format().username?._errors || [];
return Response.json(
{
success: false,
message:
usernameErrors?.length > 0
? usernameErrors.join(', ')
: 'Invalid query parameters',
},
{ status: 400 }
);
}
const { username } = result.data;
const existingVerifiedUser = await UserModel.findOne({
username,
isVerified: true,
});
if (existingVerifiedUser) {
return Response.json(
{
success: false,
message: 'Username is already taken',
},
{ status: 200 }
);
}
return Response.json(
{
success: true,
message: 'Username is unique',
},
{ status: 200 }
);
} catch (error) {
console.error('Error checking username:', error);
return Response.json(
{
success: false,
message: 'Error checking username',
},
{ status: 500 }
);
}
}
//src/app/api/verify-code/route.ts
import dbConnect from '@/lib/dbConnect';
import UserModel from '@/model/User';
export async function POST(request: Request) {
// Connect to the database
await dbConnect();
try {
const { username, code } = await request.json();
const decodedUsername = decodeURIComponent(username);
const user = await UserModel.findOne({ username: decodedUsername });
if (!user) {
return Response.json(
{ success: false, message: 'User not found' },
{ status: 404 }
);
}
// Check if the code is correct and not expired
const isCodeValid = user.verifyCode === code;
const isCodeNotExpired = new Date(user.verifyCodeExpiry) > new Date();
if (isCodeValid && isCodeNotExpired) {
// Update the user's verification status
user.isVerified = true;
await user.save();
return Response.json(
{ success: true, message: 'Account verified successfully' },
{ status: 200 }
);
} else if (!isCodeNotExpired) {
// Code has expired
return Response.json(
{
success: false,
message:
'Verification code has expired. Please sign up again to get a new code.',
},
{ status: 400 }
);
} else {
// Code is incorrect
return Response.json(
{ success: false, message: 'Incorrect verification code' },
{ status: 400 }
);
}
} catch (error) {
console.error('Error verifying user:', error);
return Response.json(
{ success: false, message: 'Error verifying user' },
{ status: 500 }
);
}
}
Message API with aggregation pipeline
//src/app/api/accept-messages/route.ts
// getServerSession --> to access session from nextAuth -> set by callback (where all data is stored [user-->token-->session])
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]/options';
import dbConnect from '@/lib/dbConnect';
import UserModel from '@/model/User';
import { User } from 'next-auth';
export async function POST(request: Request) {
// Connect to the database
await dbConnect();
const session = await getServerSession(authOptions);
const user: User = session?.user;
if (!session || !session.user) {
return Response.json(
{ success: false, message: 'Not authenticated' },
{ status: 401 }
);
}
const userId = user._id;
const { acceptMessages } = await request.json();
try {
// Update the user's message acceptance status
const updatedUser = await UserModel.findByIdAndUpdate(
userId,
{ isAcceptingMessages: acceptMessages },
{ new: true } // get updated value
);
if (!updatedUser) {
// User not found
return Response.json(
{
success: false,
message: 'Unable to find user to update message acceptance status',
},
{ status: 404 }
);
}
// Successfully updated message acceptance status
return Response.json(
{
success: true,
message: 'Message acceptance status updated successfully',
updatedUser,
},
{ status: 200 }
);
} catch (error) {
console.error('Error updating message acceptance status:', error);
return Response.json(
{ success: false, message: 'Error updating message acceptance status' },
{ status: 500 }
);
}
}
export async function GET(request: Request) {
// Connect to the database
await dbConnect();
// Get the user session
const session = await getServerSession(authOptions);
const user = session?.user;
// Check if the user is authenticated
if (!session || !user) {
return Response.json(
{ success: false, message: 'Not authenticated' },
{ status: 401 }
);
}
try {
// Retrieve the user from the database using the ID
const foundUser = await UserModel.findById(user._id);
if (!foundUser) {
// User not found
return Response.json(
{ success: false, message: 'User not found' },
{ status: 404 }
);
}
// Return the user's message acceptance status
return Response.json(
{
success: true,
isAcceptingMessages: foundUser.isAcceptingMessages,
},
{ status: 200 }
);
} catch (error) {
console.error('Error retrieving message acceptance status:', error);
return Response.json(
{ success: false, message: 'Error retrieving message acceptance status' },
{ status: 500 }
);
}
}
//src/app/api/get-messages/route.ts
// aggregation
import dbConnect from '@/lib/dbConnect';
import UserModel from '@/model/User';
import mongoose from 'mongoose';
import { User } from 'next-auth';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]/options';
export async function GET(request: Request) {
await dbConnect();
const session = await getServerSession(authOptions);
const _user: User = session?.user;
if (!session || !_user) {
return Response.json(
{ success: false, message: 'Not authenticated' },
{ status: 401 }
);
}
// convert string to mongoose object -> id
// since we are going to use aggregation pipeline
const userId = new mongoose.Types.ObjectId(_user._id);
try {
const user = await UserModel.aggregate([
{ $match: { _id: userId } },
{ $unwind: '$messages' },
{ $sort: { 'messages.createdAt': -1 } },
{ $group: { _id: '$_id', messages: { $push: '$messages' } } },
]).exec();
if (!user || user.length === 0) {
return Response.json(
{ message: 'User not found', success: false },
{ status: 404 }
);
}
return Response.json(
{ messages: user[0].messages },
{
status: 200,
}
);
} catch (error) {
console.error('An unexpected error occurred:', error);
return Response.json(
{ message: 'Internal server error', success: false },
{ status: 500 }
);
}
}
//src/app/api/send-message/route.ts
import UserModel from '@/model/User';
import dbConnect from '@/lib/dbConnect';
import { Message } from '@/model/User';
export async function POST(request: Request) {
await dbConnect();
const { username, content } = await request.json();
try {
const user = await UserModel.findOne({ username }).exec();
if (!user) {
return Response.json(
{ message: 'User not found', success: false },
{ status: 404 }
);
}
// Check if the user is accepting messages
if (!user.isAcceptingMessages) {
return Response.json(
{ message: 'User is not accepting messages', success: false },
{ status: 403 } // 403 Forbidden status
);
}
const newMessage = { content, createdAt: new Date() };
// Push the new message to the user's messages array
user.messages.push(newMessage as Message);
await user.save();
return Response.json(
{ message: 'Message sent successfully', success: true },
{ status: 201 }
);
} catch (error) {
console.error('Error adding message:', error);
return Response.json(
{ message: 'Internal server error', success: false },
{ status: 500 }
);
}
}
Integrating AI features in NextJS project
npm i ai openai
//src/app/api/suggest-message/route.ts
import OpenAI from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { NextResponse } from 'next/server';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export const runtime = 'edge'; // since backend in nextjs is not running every time
export async function POST(req: Request) {
try {
const prompt =
"Create a list of three open-ended and engaging questions formatted as a single string. Each question should be separated by '||'. These questions are for an anonymous social messaging platform, like Qooh.me, and should be suitable for a diverse audience. Avoid personal or sensitive topics, focusing instead on universal themes that encourage friendly interaction. For example, your output should be structured like this: 'What’s a hobby you’ve recently started?||If you could have dinner with any historical figure, who would it be?||What’s a simple thing that makes you happy?'. Ensure the questions are intriguing, foster curiosity, and contribute to a positive and welcoming conversational environment.";
const response = await openai.completions.create({
model: 'gpt-3.5-turbo-instruct',
max_tokens: 400,
stream: true,
prompt,
});
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
} catch (error) {
if (error instanceof OpenAI.APIError) {
// OpenAI API error handling
const { name, status, headers, message } = error;
return NextResponse.json({ name, status, headers, message }, { status });
} else {
// General error handling
console.error('An unexpected error occurred:', error);
throw error;
}
}
}
frontend
React hook form, shadcn and debouncing
npx shadcn-ui@latest init
// style -> default
// base color -> Slate
// css variable --> yes
npx shadcn-ui@latest add form
// it automatically create components --> button,label,form
// install all dependency of form
// src/components/ui/button.tsx
// src/components/ui/label.tsx
// src/components/ui/form.tsx
npm i axios
npm i usehooks-ts
npx shadcn-ui@latest add toast
//src/context/AuthProvider.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export default function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<SessionProvider>
{children}
</SessionProvider>
);
}
//src/app/layout.tsx --> ~index.js in recat
// Toaster
// AuthProvider
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import AuthProvider from '../context/AuthProvider';
import { Toaster } from '@/components/ui/toaster';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'True Feedback',
description: 'Real feedback from real people.',
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default async function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" >
<AuthProvider>
<body className={inter.className}>
{children}
<Toaster />
</body>
</AuthProvider>
</html>
);
}
//src/app/(auth)/sign-up/page.tsx
//useToast
//useRouter
//useDebounce
'use client';
import { ApiResponse } from '@/types/ApiResponse';
import { zodResolver } from '@hookform/resolvers/zod';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useDebounce } from 'usehooks-ts';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/use-toast';
import axios, { AxiosError } from 'axios';
import { Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { signUpSchema } from '@/schemas/signUpSchema';
export default function SignUpForm() {
const [username, setUsername] = useState('');
const [usernameMessage, setUsernameMessage] = useState('');
const [isCheckingUsername, setIsCheckingUsername] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const debouncedUsername = useDebounce(username, 300);
const router = useRouter();
const { toast } = useToast();
const form = useForm<z.infer<typeof signUpSchema>>({
resolver: zodResolver(signUpSchema),
defaultValues: {
username: '',
email: '',
password: '',
},
});
useEffect(() => {
const checkUsernameUnique = async () => {
if (debouncedUsername) {
setIsCheckingUsername(true);
setUsernameMessage(''); // Reset message
try {
const response = await axios.get<ApiResponse>(
`/api/check-username-unique?username=${debouncedUsername}`
);
setUsernameMessage(response.data.message);
} catch (error) {
const axiosError = error as AxiosError<ApiResponse>;
setUsernameMessage(
axiosError.response?.data.message ?? 'Error checking username'
);
} finally {
setIsCheckingUsername(false);
}
}
};
checkUsernameUnique();
}, [debouncedUsername]);
const onSubmit = async (data: z.infer<typeof signUpSchema>) => {
setIsSubmitting(true);
try {
const response = await axios.post<ApiResponse>('/api/sign-up', data);
toast({
title: 'Success',
description: response.data.message,
});
router.replace(`/verify/${username}`);
setIsSubmitting(false);
} catch (error) {
console.error('Error during sign-up:', error);
const axiosError = error as AxiosError<ApiResponse>;
// Default error message
let errorMessage = axiosError.response?.data.message;
('There was a problem with your sign-up. Please try again.');
toast({
title: 'Sign Up Failed',
description: errorMessage,
variant: 'destructive',
});
setIsSubmitting(false);
}
};
return (
<div className="flex justify-center items-center min-h-screen bg-gray-800">
<div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl mb-6">
Join True Feedback
</h1>
<p className="mb-4">Sign up to start your anonymous adventure</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
name="username"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<Input
{...field}
onChange={(e) => {
field.onChange(e);
setUsername(e.target.value);
}}
/>
{isCheckingUsername && <Loader2 className="animate-spin" />}
{!isCheckingUsername && usernameMessage && (
<p
className={`text-sm ${
usernameMessage === 'Username is unique'
? 'text-green-500'
: 'text-red-500'
}`}
>
{usernameMessage}
</p>
)}
<FormMessage />
</FormItem>
)}
/>
<FormField
name="email"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Input {...field} name="email" />
<p className='text-muted text-gray-400 text-sm'>We will send you a verification code</p>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<Input type="password" {...field} name="password" />
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className='w-full' disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</>
) : (
'Sign Up'
)}
</Button>
</form>
</Form>
<div className="text-center mt-4">
<p>
Already a member?{' '}
<Link href="/sign-in" className="text-blue-600 hover:text-blue-800">
Sign in
</Link>
</p>
</div>
</div>
</div>
);
}
OTP verification in NextJS
//src/app/(auth)/verify/[username]/page.tsx
'use client';
import { Button } from '@/components/ui/button';
import {
Form,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/use-toast';
import { ApiResponse } from '@/types/ApiResponse';
import { zodResolver } from '@hookform/resolvers/zod';
import axios, { AxiosError } from 'axios';
import { useParams, useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { verifySchema } from '@/schemas/verifySchema';
export default function VerifyAccount() {
const router = useRouter();
const params = useParams<{ username: string }>();
const { toast } = useToast();
const form = useForm<z.infer<typeof verifySchema>>({
resolver: zodResolver(verifySchema),
});
const onSubmit = async (data: z.infer<typeof verifySchema>) => {
try {
const response = await axios.post<ApiResponse>(`/api/verify-code`, {
username: params.username,
code: data.code,
});
toast({
title: 'Success',
description: response.data.message,
});
router.replace('/sign-in');
} catch (error) {
const axiosError = error as AxiosError<ApiResponse>;
toast({
title: 'Verification Failed',
description:
axiosError.response?.data.message ??
'An error occurred. Please try again.',
variant: 'destructive',
});
}
};
return (
<div className="flex justify-center items-center min-h-screen bg-gray-100">
<div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl mb-6">
Verify Your Account
</h1>
<p className="mb-4">Enter the verification code sent to your email</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
name="code"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Verification Code</FormLabel>
<Input {...field} />
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Verify</Button>
</form>
</Form>
</div>
</div>
);
}
Handling signin with AuthJS
//src/app/(auth)/sign-in/page.tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { signIn } from 'next-auth/react';
import {
Form,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useToast } from '@/components/ui/use-toast';
import { signInSchema } from '@/schemas/signInSchema';
export default function SignInForm() {
const router = useRouter();
const form = useForm<z.infer<typeof signInSchema>>({
resolver: zodResolver(signInSchema),
defaultValues: {
identifier: '',
password: '',
},
});
const { toast } = useToast();
const onSubmit = async (data: z.infer<typeof signInSchema>) => {
const result = await signIn('credentials', {
redirect: false,
identifier: data.identifier,
password: data.password,
});
if (result?.error) {
if (result.error === 'CredentialsSignin') {
toast({
title: 'Login Failed',
description: 'Incorrect username or password',
variant: 'destructive',
});
} else {
toast({
title: 'Error',
description: result.error,
variant: 'destructive',
});
}
}
if (result?.url) {
router.replace('/dashboard');
}
};
return (
<div className="flex justify-center items-center min-h-screen bg-gray-800">
<div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl mb-6">
Welcome Back to True Feedback
</h1>
<p className="mb-4">Sign in to continue your secret conversations</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
name="identifier"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email/Username</FormLabel>
<Input {...field} />
<FormMessage />
</FormItem>
)}
/>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<Input type="password" {...field} />
<FormMessage />
</FormItem>
)}
/>
<Button className='w-full' type="submit">Sign In</Button>
</form>
</Form>
<div className="text-center mt-4">
<p>
Not a member yet?{' '}
<Link href="/sign-up" className="text-blue-600 hover:text-blue-800">
Sign up
</Link>
</p>
</div>
</div>
</div>
);
}
Navbar and message card with bug fixes in Nextjs
//src/components/navbar.tsx
//useSession() --> {data: session} --> rename data as session
'use client'
import React from 'react';
import Link from 'next/link';
import { useSession, signOut } from 'next-auth/react';
import { Button } from './ui/button';
import { User } from 'next-auth';
function Navbar() {
const { data: session } = useSession();
const user : User = session?.user as User; // assersion --> as User
return (
<nav className="p-4 md:p-6 shadow-md bg-gray-900 text-white">
<div className="container mx-auto flex flex-col md:flex-row justify-between items-center">
<a href="#" className="text-xl font-bold mb-4 md:mb-0">
True Feedback
</a>
{session ? (
<>
<span className="mr-4">
Welcome, {user.username || user.email}
</span>
<Button onClick={() => signOut()} className="w-full md:w-auto bg-slate-100 text-black" variant='outline'>
Logout
</Button>
</>
) : (
<Link href="/sign-in">
<Button className="w-full md:w-auto bg-slate-100 text-black" variant={'outline'}>Login</Button>
</Link>
)}
</div>
</nav>
);
}
export default Navbar;
//src/components/MessageCard.tsx
'use client'
import React, { useState } from 'react';
import axios, { AxiosError } from 'axios';
import dayjs from 'dayjs';
import { X } from 'lucide-react';
import { Message } from '@/model/User';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Button } from './ui/button';
import { useToast } from '@/components/ui/use-toast';
import { ApiResponse } from '@/types/ApiResponse';
type MessageCardProps = {
message: Message;
onMessageDelete: (messageId: string) => void;
};
export function MessageCard({ message, onMessageDelete }: MessageCardProps) {
const { toast } = useToast();
const handleDeleteConfirm = async () => {
try {
const response = await axios.delete<ApiResponse>(
`/api/delete-message/${message._id}`
);
toast({
title: response.data.message,
});
onMessageDelete(message._id);
} catch (error) {
const axiosError = error as AxiosError<ApiResponse>;
toast({
title: 'Error',
description:
axiosError.response?.data.message ?? 'Failed to delete message',
variant: 'destructive',
});
}
};
return (
<Card className="card-bordered">
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>{message.content}</CardTitle>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant='destructive'>
<X className="w-5 h-5" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete
this message.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<div className="text-sm">
{dayjs(message.createdAt).format('MMM D, YYYY h:mm A')}
</div>
</CardHeader>
<CardContent></CardContent>
</Card>
);
}
//src/(app)/layout.ts
import Navbar from '@/components/Navbar';
interface RootLayoutProps {
children: React.ReactNode;
}
export default async function RootLayout({ children }: RootLayoutProps) {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
{children}
</div>
);
}
14 . We forgot delete Route backend
// src/app/api/delete-message/[messageId]/route.ts
import UserModel from '@/model/User';
import { getServerSession } from 'next-auth/next';
import dbConnect from '@/lib/dbConnect';
import { User } from 'next-auth';
import { Message } from '@/model/User';
import { NextRequest } from 'next/server';
import { authOptions } from '../../auth/[...nextauth]/options';
export async function DELETE(
request: Request,
{ params }: { params: { messageid: string } }
) {
const messageId = params.messageid;
await dbConnect();
const session = await getServerSession(authOptions);
const _user: User = session?.user as User;
if (!session || !_user) {
return Response.json(
{ success: false, message: 'Not authenticated' },
{ status: 401 }
);
}
try {
const updateResult = await UserModel.updateOne(
{ _id: _user._id },
{ $pull: { messages: { _id: messageId } } }
);
if (updateResult.modifiedCount === 0) {
return Response.json(
{ message: 'Message not found or already deleted', success: false },
{ status: 404 }
);
}
return Response.json(
{ message: 'Message deleted', success: true },
{ status: 200 }
);
} catch (error) {
console.error('Error deleting message:', error);
return Response.json(
{ message: 'Error deleting message', success: false },
{ status: 500 }
);
}
}
Building User Dashboard
// npx shadcn-ui@latest add separator switch
// watch ,setValue
// app/(app)/dashboard/page.tsx
'use client';
import { MessageCard } from '@/components/MessageCard';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { Message } from '@/model/User';
import { ApiResponse } from '@/types/ApiResponse';
import { zodResolver } from '@hookform/resolvers/zod';
import axios, { AxiosError } from 'axios';
import { Loader2, RefreshCcw } from 'lucide-react';
import { User } from 'next-auth';
import { useSession } from 'next-auth/react';
import React, { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { AcceptMessageSchema } from '@/schemas/acceptMessageSchema';
function UserDashboard() {
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isSwitchLoading, setIsSwitchLoading] = useState(false);
const { toast } = useToast();
const handleDeleteMessage = (messageId: string) => {
setMessages(messages.filter((message) => message._id !== messageId));
};
const { data: session } = useSession();
const form = useForm({
resolver: zodResolver(AcceptMessageSchema),
});
const { register, watch, setValue } = form;
const acceptMessages = watch('acceptMessages');
const fetchAcceptMessages = useCallback(async () => {
setIsSwitchLoading(true);
try {
const response = await axios.get<ApiResponse>('/api/accept-messages');
setValue('acceptMessages', response.data.isAcceptingMessages);
} catch (error) {
const axiosError = error as AxiosError<ApiResponse>;
toast({
title: 'Error',
description:
axiosError.response?.data.message ??
'Failed to fetch message settings',
variant: 'destructive',
});
} finally {
setIsSwitchLoading(false);
}
}, [setValue, toast]);
const fetchMessages = useCallback(
async (refresh: boolean = false) => {
setIsLoading(true);
setIsSwitchLoading(false);
try {
const response = await axios.get<ApiResponse>('/api/get-messages');
setMessages(response.data.messages || []);
if (refresh) {
toast({
title: 'Refreshed Messages',
description: 'Showing latest messages',
});
}
} catch (error) {
const axiosError = error as AxiosError<ApiResponse>;
toast({
title: 'Error',
description:
axiosError.response?.data.message ?? 'Failed to fetch messages',
variant: 'destructive',
});
} finally {
setIsLoading(false);
setIsSwitchLoading(false);
}
},
[setIsLoading, setMessages, toast]
);
// Fetch initial state from the server
useEffect(() => {
if (!session || !session.user) return;
fetchMessages();
fetchAcceptMessages();
}, [session, setValue, toast, fetchAcceptMessages, fetchMessages]);
// Handle switch change
const handleSwitchChange = async () => {
try {
const response = await axios.post<ApiResponse>('/api/accept-messages', {
acceptMessages: !acceptMessages,
});
setValue('acceptMessages', !acceptMessages);
toast({
title: response.data.message,
variant: 'default',
});
} catch (error) {
const axiosError = error as AxiosError<ApiResponse>;
toast({
title: 'Error',
description:
axiosError.response?.data.message ??
'Failed to update message settings',
variant: 'destructive',
});
}
};
if (!session || !session.user) {
return <div></div>;
}
const { username } = session.user as User;
const baseUrl = `${window.location.protocol}//${window.location.host}`;
const profileUrl = `${baseUrl}/u/${username}`;
const copyToClipboard = () => {
navigator.clipboard.writeText(profileUrl);
toast({
title: 'URL Copied!',
description: 'Profile URL has been copied to clipboard.',
});
};
return (
<div className="my-8 mx-4 md:mx-8 lg:mx-auto p-6 bg-white rounded w-full max-w-6xl">
<h1 className="text-4xl font-bold mb-4">User Dashboard</h1>
<div className="mb-4">
<h2 className="text-lg font-semibold mb-2">Copy Your Unique Link</h2>{' '}
<div className="flex items-center">
<input
type="text"
value={profileUrl}
disabled
className="input input-bordered w-full p-2 mr-2"
/>
<Button onClick={copyToClipboard}>Copy</Button>
</div>
</div>
<div className="mb-4">
<Switch
{...register('acceptMessages')}
checked={acceptMessages}
onCheckedChange={handleSwitchChange}
disabled={isSwitchLoading}
/>
<span className="ml-2">
Accept Messages: {acceptMessages ? 'On' : 'Off'}
</span>
</div>
<Separator />
<Button
className="mt-4"
variant="outline"
onClick={(e) => {
e.preventDefault();
fetchMessages(true);
}}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCcw className="h-4 w-4" />
)}
</Button>
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-6">
{messages.length > 0 ? (
messages.map((message, index) => (
<MessageCard
key={message._id}
message={message}
onMessageDelete={handleDeleteMessage}
/>
))
) : (
<p>No messages to display.</p>
)}
</div>
</div>
);
}
export default UserDashboard;
Struggle with Carousel shadcn in NextJS
// npx shadcn-ui@latest add carousel
// app/(app)/page.tsx
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Mail } from 'lucide-react'; // Assuming you have an icon for messages
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import Autoplay from 'embla-carousel-autoplay';
import messages from '@/messages.json';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@/components/ui/carousel';
export default function Home() {
return (
<>
{/* Main content */}
<main className="flex-grow flex flex-col items-center justify-center px-4 md:px-24 py-12 bg-gray-800 text-white">
<section className="text-center mb-8 md:mb-12">
<h1 className="text-3xl md:text-5xl font-bold">
Dive into the World of Anonymous Feedback
</h1>
<p className="mt-3 md:mt-4 text-base md:text-lg">
True Feedback - Where your identity remains a secret.
</p>
</section>
{/* Carousel for Messages */}
<Carousel
plugins={[Autoplay({ delay: 2000 })]}
className="w-full max-w-lg md:max-w-xl"
>
<CarouselContent>
{messages.map((message, index) => (
<CarouselItem key={index} className="p-4">
<Card>
<CardHeader>
<CardTitle>{message.title}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col md:flex-row items-start space-y-2 md:space-y-0 md:space-x-4">
<Mail className="flex-shrink-0" />
<div>
<p>{message.content}</p>
<p className="text-xs text-muted-foreground">
{message.received}
</p>
</div>
</CardContent>
</Card>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</main>
{/* Footer */}
<footer className="text-center p-4 md:p-6 bg-gray-900 text-white">
© 2023 True Feedback. All rights reserved.
</footer>
</>
);
}