Interio Full Stack
Interio Full Stack
Introduction
Interio is a platform for interior designers to share work, get inspiration, and
showcase designs
Key features include user registration, design posting with images, tag-based
design browsing, and personal collections
The app structure includes pages for home, designs, profile, and shot uploads
Technologies Used
Nextjs 14 (App Router)
Server Actions
TypeScript
Uploadthing
Uploadthing
Project Setup
Create a new directory for the project.
Paste the below command to initialize a nextjs project. (you may also use
npm/yarn/bun)
pnpx create-next-app@latest
{
"name": "interio",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,cs
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@portabletext/react": "^3.1.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
App Structure
├── app
│ ├── layout.tsx
│ ├── page.tsx
│ ├── shots
│ │ └── page.tsx
│ ├── profile
│ │ └── page.tsx
├── components
│ ├── ui
│ │ ├── alert-dialog.tsx
│ │ ├── button.tsx
│ │ ├── dropdown-menu.tsx
│ │ └── ...
│ ├── main-nav.tsx
│ ├── page-header.tsx
JWT_SECRET=
MONGODB_URL=
UPLOADTHING_SECRET=
UPLOADTHING_APP_ID=
Types
Types used across the project.
APIs Setup
1. MongoDB connection in the nextjs environment.
try {
await mongoose.connect(process.env.MONGODB_URL)
mongoose.set("strictQuery", true)
vendorSchema.virtual("Shot", {
ref: "shotModel",
localField: "_id",
foreignField: "owner",
})
vendorSchema.methods.toJSON = function () {
const vendor = this
const vendorObject = vendor.toObject()
delete vendorObject.password
delete vendorObject.tokens
delete vendorObject.otp
return vendorObject
}
Shots
mongoose.set("strictQuery", true)
Collection
mongoose.set("strictQuery", true)
3. Server Actions
Vendor Actions
"use server"
//-------------NEW VENDOR-------------//
export const addVendor = async (body: any) => {
try {
connectToDB()
//-------------LOGIN VENDOR-------------//
export const vendorLogin = async (body: any) => {
try {
connectToDB()
const vendor = await VENDOR.findByCredentials(body.ema
if (!vendor) {
throw new Error("Invalid Attempt, vendor not Found!"
}
const token = await vendor.generateAuthToken()
cookies().set("token", token)
return { name: vendor?.name, _id: vendor?._id.toString
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}
//-------------LOGOUT VENDOR-------------//
export const vendorLogout = async () => {
try {
const cookieStore = cookies()
if (!_id) {
return { message: "Token Expired", code: 500 }
}
const vendor = await VENDOR.findOne({ _id })
if (!vendor) {
throw new Error("Invalid Attempt, vendor not Found!"
}
cookies().delete("token")
vendor.tokens = vendor.tokens.filter((tok) => {
return tok.token !== token?.value
})
await vendor.save()
return { message: "Logged Out!" }
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}
//-------------REVALIDATE VENDOR-------------//
export const revalidateVendor = async () => {
try {
connectToDB()
//-------------UPDATE VENDOR-------------//
Shot Actions
"use server"
//-------------NEW SHOT-------------//
export const addShot = async (body: any) => {
try {
connectToDB()
const shot = await new SHOT(body)
await shot.save()
if (!shot || !vendor) {
throw new Error("Unable to add shot!")
}
vendor.ownShot.push(shot._id)
return { shot }
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}
if (!shot) {
throw Error("NO SHOT FOUND")
}
return shot
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}
/vendor/route.ts
/shot/route.ts
return NextResponse.json(shot)
} catch (error) {
console.error(error)
return NextResponse.error()
}
}
/shot/[shotId]/route.ts
return NextResponse.json(shot)
} catch (error) {
return NextResponse.json({ error })
}
}
/uploadthing/route.ts
const f = createUploadthing()
Frontend(UI) Setup
Here, It has mainly 5-6 pages (routes).
├── /home
│ ├── /designs
│ │ ├── /upload
│ │ └── /[shotId]
│ ├── /profile
│ │ └── /edit
Home Page
page.tsx
return (
<section className="bg-dark">
<Hero />
<IconList />
<RectangleCard />
{/* FOOTER */}
<div className="z-0 flex h-auto flex-col items-center j
<Link href="/">
<Image src={"/interio.png"} alt="interio logo" heig
</Link>
<p className="px-2 md:px-6 lg:px-10">
Modern Designs | Minimal Designs | Luxurious Design
Terms & Conditions Privacy Policy | About us
</p>
💖 </p>
<p>Handcrafted by Interio © {new Date().getFullYear()
<p>Made with
</div>
</section>
layout.tsx
import "./globals.css"
Design Pages
/design/page.tsx
type Props = {
searchParams: { type: string }
}
export default async function Designs({ searchParams }: Props
const type = searchParams.type
const shots: shotData[] = []
try {
const data = await getShot(type)
if (data) {
shots.push(...(data.shots as shotData[]))
}
} catch (error) {
console.log(error)
return (
<>
{shots?.length > 0 ? (
<div className="grid grid-cols-1 justify-items-center
{shots.map((shot: shotData) => (
<ShotCard key={shot._id} shot={shot} />
))}
</div>
) : (
<p className="text-center">Nothing to show!</p>
)}
</>
)
}
/design/[shotId]/page.tsx
return (
<>
<Image
src={shot?.images[0]?.url ? shot.images[0].url : "/gr
className="h-[700px] w-[1200px] rounded"
alt="bed"
height={500}
width={1400}
/>
<div className="my-8 flex justify-between">
<div className="flex gap-x-4">
<Image src={shot.owner.profilePic || "/user.png"} h
<div>
<h1>{shot?.title}</h1>
<p className="text-gray text-xs">{shot?.owner?.na
</div>
</div>
<div className="flex gap-x-4 ">
<button className="cborder bg-trans rounded px-4 py
<button className="bg-trans rounded border border-p
<Image src={"/pheart.png"} alt="heart-icon" heigh
</button>
</div>
</div>
<p>{shot?.description}</p>
📪
<p className="my-4">
Email:
<a href={`mailto:${shot.owner.email}`}>
<span className="text-primary"> {shot?.owner?.email
</a>
</p>
<p className="border-gray my-8 w-full border-[0.5px]" /
{/* {shot?.owner?._id === vendor.v_id && (
<div className='my-8 flex items-center justify-ce
<button className='cborder rounded bg-trans px-
Edit
</button>
<button className='cborder rounded bg-trans px-
Edit Details
</button>
<button className='cborder rounded bg-trans px-
Delete
</button>
</div>
)} */}
/design/upload/page.tsx
"use client"
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} classNam
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="enter title" {...field}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea rows={12} placeholder="Tell us a
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<p className="text-muted-foreground">Upload Shot</p
<UploadDropzone
endpoint="imageUploader"
className="h-60 w-full rounded bg-background px-4
onClientUploadComplete={(res) => {
/layout.tsx
"use client"
type Props = {
children?: React.ReactNode
way?: "without" | "with"
}
return (
{loginStatus && (
<Link href="/profile" className="absolute bottom-
<Image
src={vendor?.profilePic || "/user.png"}
alt="interio logo"
height={40}
width={40}
className="h-10 w-10 rounded-full object-fill
/>
</Link>
)}
<Link
href="/designs"
className={clsx(
{ "bg-secondary/70 text-white": pathname == "/d
"flex w-[200px] items-center gap-x-2 rounded-lg
)}
>
<Icons.HiOutlinePhotograph size={24} />
<div>
<h3 className="font-sm font-medium">10k+</h3>
<p className="text-light text-xs">Inspirations
</div>
</Link>
<div
className={clsx(
{ "bg-secondary text-white": pathname == "/suit
"flex w-[200px] items-center gap-x-2 rounded-lg
)}
>
<Icons.RiSuitcaseLine size={24} />
<div>
<h3 className="font-sm font-medium">123+</h3>
<p className="text-light text-xs">
Find Work <br /> (coming soon)
</p>
</div>
</div>
<div
Profile Pages
profile/page.tsx
if (error || !vendor)
return (
<div className="text-center">
<h1 className="text-red-500">Some error at our end!</
<p className="text-gray">{error}</p>
</div>
)
<div>
<h1 className="mb-1 text-xl text-primary underline de
{vendor.workHistory.length > 0 ? (
vendor?.workHistory.map((_, i) => (
<div className="mb-4 grid grid-cols-2 items-end j
<div>
<h2 className="mb-1 text-base">{_.title}</h2>
<h3 className="flex items-center gap-2 lg:ml-
<Icons.RiSuitcaseLine size={24} className="
</h3>
</div>
<p className="flex items-center gap-1">
<Icons.Location size={24} className="text-whi
</p>
<p className="flex items-center gap-1">
<Icons.Calender size={24} className="text-whi
</p>
<p className="flex items-center gap-1">
<Icons.Calender size={24} className="text-whi
<div>
<h1 className="mb-1 text-xl text-primary underline
{vendor.lookingFor.length > 0 ? (
<div className="text-foreground">
{vendor?.lookingFor.map((_, i) => (
<div key={i} className="mb-2">
<h2 className="mb-1 text-base">{_.title}</h
<h3 className="flex items-center gap-2 lg:m
<Icons.Location size={24} className="text
</h3>
</div>
))}
</div>
) : (
<p className="text-foreground">No looking for fou
)}
</div>
</div>
</section>
)
}
profile/edit/page.ts
interface IFormInput {
name: string
profilePic: string
email: string
contact: number
address: string
biography: string
workHistory: object
lookingFor: object
skills: string[]
}
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} classNam
<div className="relative -mt-16 flex items-center
<div className="flex items-center gap-4">
<Image
src={form.getValues("profilePic") || "/user.p
alt="user"
width={80}
height={80}
className="h-12 w-12 rounded-full md:h-20 md
/>
<Icons.Edit size={24} />
</div>
<Link
className={buttonVariants({
profile/[tab]/page.tsx
/layout.tsx
"use client"
if (!loginStatus) {
<Link
href={"/profile/liked-shot"}
className={`${buttonVariants({
variant: "secondary",
})} ${pathname == "/profile/liked-shot" ?
>
Liked Shots
</Link>
<Link
href={"/profile/collection"}
className={`${buttonVariants({
variant: "secondary",
})} ${pathname == "/profile/collection" ?
`}
>
Collections
</Link>
LeftSideBar - component
"use client"
type Props = {
children?: React.ReactNode
way?: "without" | "with"
}
return (
<>
<aside className="fixed hidden h-screen md:flex">
<div className="flex h-screen w-[60px] flex-col items
<Link href="/">
<Image src={"/interio.png"} alt="interio logo" he
</Link>
<Link href="/designs">
<Icons.AiFillAppstore />
</Link>
<Link href="/profile">
<Icons.AiOutlineUser />
</Link>
<button onClick={() => dispatch(togglePanel("invite
<Icons.HiOutlineMail />
</button>
<button onClick={() => dispatch(togglePanel("collec
<Icons.FiFolderMinus />
</button>
<p className="border-gray w-[2vw] border"> </p>
<button title="Coming Soon!">
<Icons.FiSettings />
</button>
{loginStatus && (
<Link href="/profile" className="absolute bottom-
<Image
src={vendor?.profilePic || "/user.png"}
alt="interio logo"
height={40}
width={40}
<Link
href="/designs"
className={clsx(
{ "bg-secondary/70 text-white": pathname == "/d
"flex w-[200px] items-center gap-x-2 rounded-lg
)}
>
<Icons.HiOutlinePhotograph size={24} />
<div>
<h3 className="font-sm font-medium">10k+</h3>
<p className="text-light text-xs">Inspirations
</div>
</Link>
<div
className={clsx(
{ "bg-secondary text-white": pathname == "/suit
"flex w-[200px] items-center gap-x-2 rounded-lg
)}
>
<Icons.RiSuitcaseLine size={24} />
<div>
<h3 className="font-sm font-medium">123+</h3>
<p className="text-light text-xs">
Find Work <br /> (coming soon)
Context Setup
hook.ts
store.ts
theme.ts
Conclusion
This project documentation provides a comprehensive overview of the Interio Full
Stack application, detailing its various components and functionalities. From user
profiles to vendor interactions, the documentation covers all essential aspects to
ensure a smooth and efficient development process.
Useful Resources
1. https://nextjs.org/
2. https://ui.shadcn.com/
3. https://tailwindcss.com/
4. https://react-hook-form.com/
5. https://www.mongodb.com/
6. https://mongoosejs.com/
7. https://joi.dev/
8. https://uploadthing.com/