0% found this document useful (0 votes)
47 views71 pages

Interio Full Stack

Interio is a full-stack platform designed for interior designers to share their work and gain inspiration, featuring user registration, design posting, and personal collections. The project utilizes technologies such as Next.js, Tailwind CSS, MongoDB, and TypeScript, with a focus on a clean and modern UI. The document outlines the app structure, project setup, environment variables, utility functions, and database schema design for vendors, shots, and collections.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
47 views71 pages

Interio Full Stack

Interio is a full-stack platform designed for interior designers to share their work and gain inspiration, featuring user registration, design posting, and personal collections. The project utilizes technologies such as Next.js, Tailwind CSS, MongoDB, and TypeScript, with a focus on a clean and modern UI. The document outlines the app structure, project setup, environment variables, utility functions, and database schema design for vendors, shots, and collections.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 71

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

Server actions handle vendor and shot-related operations, including


authentication and CRUD functions

The frontend UI is responsive and includes features like design browsing,


profile editing, and shot uploads

User profiles display work history, skills, and design collections

The project emphasizes a clean, modern design with a focus on showcasing


interior design work

Technologies Used
Nextjs 14 (App Router)

Tailwind CSS with shadcn/ui

Server Actions

TypeScript

React Hook Forms

Uploadthing

Interio - Full Stack 1


MongoDB

Uploadthing

Project Setup
Create a new directory for the project.

Sign up to the uploadthing website and get the UPLOADTHING_SECRET and


UPLOADTHING_APP_ID.

Paste the below command to initialize a nextjs project. (you may also use
npm/yarn/bun)

pnpx create-next-app@latest

Exchange the pakage.json file to install all dependencies.

{
"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",

Interio - Full Stack 2


"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@sanity/client": "^6.21.1",
"@sanity/image-url": "^1.0.2",
"@sanity/vision": "^3.53.0",
"@types/node": "22.1.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@uploadthing/react": "^6.7.2",
"autoprefixer": "10.4.20",
"axios": "^1.7.3",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"eslint": "9.8.0",
"eslint-config-next": "14.2.5",
"eslint-plugin-unicorn": "^55.0.0",
"framer-motion": "^11.3.24",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.426.0",
"mongoose": "^8.5.2",
"next": "14.2.5",
"next-themes": "^0.3.0",
"postcss": "8.4.41",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.52.2",
"react-icons": "^5.2.1",
"react-redux": "^9.1.2",
"sanity": "^3.53.0",
"styled-components": "^6.1.12",
"tailwind-merge": "^2.4.0",
"tailwind-scrollbar": "^3.1.0",

Interio - Full Stack 3


"tailwindcss": "3.4.9",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.5.4",
"uploadthing": "^6.13.2",
"vaul": "^0.9.1"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@tailwindcss/typography": "^0.5.14",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5"
}
}

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

Interio - Full Stack 4


│ └── ...
├── context
│ ├── hook.ts
│ ├── store.ts
│ └── theme.ts
├── lib
│ ├── utils.ts
│ ├── actions
│ │ ├── shot.action.ts
│ │ └── vendor.action.ts
│ ├── models
│ │ ├── collection.model.ts
│ │ ├── shot.model.ts
│ │ └── vendor.model.ts
│ └── db.ts
├── styles
│ └── globals.css
├── next.config.js
├── package.json
├── tailwind.config.js
└── tsconfig.json

Environment Variables (.env)

JWT_SECRET=
MONGODB_URL=
UPLOADTHING_SECRET=
UPLOADTHING_APP_ID=

Some utility functions


Below are some utility functions that will assist in various tasks across the project:

import jwt, { JwtPayload } from "jsonwebtoken"


import { generateComponents } from "@uploadthing/react"

Interio - Full Stack 5


import type { OurFileRouter } from "../app/api/uploadthing/co

const JWT_SECRET = process.env.JWT_SECRET

const getIdByToken = async (token: any) => {


try {
if (!JWT_SECRET) {
throw new Error("JWT_SECRET is not defined")
}
const decoded = jwt.verify(token.value, JWT_SECRET) as st
if (typeof decoded === "string") {
throw new Error("Invalid token")
}
return decoded._id
} catch (error) {
throw new Error(`No token : ${error}`)
}
}

export default getIdByToken

export function getErrorMessage(error: unknown): string {


let message: string
if (error instanceof Error) {
message = error.message
} else if (error && typeof error === "object" && "message"
message = String(error.message)
} else if (typeof error === "string") {
message = error
} else {
message = "Something went wrong"
}
return message
}

export function formatDateInDMY(inputDate: string): string {

Interio - Full Stack 6


const date = new Date(inputDate)
if (isNaN(date.getTime())) return ""

const options: Intl.DateTimeFormatOptions = {


day: "2-digit",
month: "short",
year: "numeric",
}
return date.toLocaleDateString(undefined, options)
}

export function formatDMY(date: string, weekday?: boolean) {


const options: {
year: string
month: string
day: string
weekday?: string
} = { year: "numeric", month: "long", day: "numeric" }
if (weekday) {
options.weekday = "short"
}
// @ts-ignore -d s
return new Date(date).toLocaleDateString(undefined, options
}

export const { UploadButton, UploadDropzone, Uploader } = gen

Types
Types used across the project.

export type OwnerType = {


name: string
follower: string[]
following: string[]

Interio - Full Stack 7


likedshot?: string[]
email: string
_id: string
}

export type shotData = {


title: string
category: string
description: string
tags: string[]
images: { title: string; url: string; _id: string }[]
_id: string
owner: OwnerType
}

export type shotDataArr = shotData[]

export type messageProp = {


sender: string
content: string
readBy: string[]
createdAt: string
}

export type vendorType = {


_id: string
name: string
follower: string[]
following: string[]
likedShot: string[]
}

export type reduxVendor = {


vendor: string
v_id: string

Interio - Full Stack 8


token: string
}

APIs Setup
1. MongoDB connection in the nextjs environment.

import mongoose from "mongoose"

let isConnected = false // Variable to track the connection s

export const connectToDB = async () => {


// Set strict query mode for Mongoose to prevent unknown fi
mongoose.set("strictQuery", true)

if (!process.env.MONGODB_URL) return console.log("Missing M

// If the connection is already established, return without


if (isConnected) {
console.log("MongoDB connection already established")
return
}

try {
await mongoose.connect(process.env.MONGODB_URL)

isConnected = true // Set the connection status to true


console.log("MongoDB connected")
} catch (error) {
console.log(error)
}
}

2. Database Schema Design

Interio - Full Stack 9


Vendor

import bcrypt from "bcryptjs"


import jwt from "jsonwebtoken"
import mongoose from "mongoose"

import { IVendor, IVendorModel } from "./types/vendor-type

mongoose.set("strictQuery", true)

const vendorSchema = new mongoose.Schema<IVendor, IVendorM


{
type: {
type: String,
default: "vendor",
},
socketId: {
type: String,
},
name: {
type: String,
trim: true,
required: true,
},
profilePic: {
type: String,
default: null,
},
username: {
type: String,
trim: true,
required: true,
unique: true,
},
email: {
type: String,

Interio - Full Stack 10


unique: true,
trim: true,
required: true,
},
contact: {
type: Number,
},
password: {
type: String,
trim: true,
required: true,
},
address: {
type: String,
trim: true,
default: null,
},
biography: {
type: String,
trim: true,
default: null,
},
workHistory: [
{
title: String,
company: String,
location: String,
from: String,
to: String,
},
],
lookingFor: [
{
title: String,
location: String,
},

Interio - Full Stack 11


],
ownShot: [mongoose.Schema.Types.ObjectId],
likedShot: [mongoose.Schema.Types.ObjectId],
shotCollections: [mongoose.Schema.Types.ObjectId],
socialLinks: [
{
platform: String,
url: String,
},
],
skills: [String],
follower: [String],
following: [String],
resetToken: String,
expireToken: Date,
otp: {
type: String,
default: null,
},
otpExpireIn: {
type: Date,
expireAfterSeconds: 150,
default: null,
},
tokens: [
{
token: {
type: String,
required: true,
},
},
],
},
{
timestamps: true,
}

Interio - Full Stack 12


)

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
}

vendorSchema.methods.generateAuthToken = async function ()


const vendor = this
const JWT_SECRET = process.env.JWT_SECRET || null
if (!JWT_SECRET) {
throw new Error("JWT secret not found")
}
const token = jwt.sign({ _id: vendor._id.toString() }, J
expiresIn: "1h",
})
vendor.tokens = vendor.tokens.concat({ token })
await vendor.save()
return token
}

vendorSchema.statics.findByCredentials = async (email, pas


const vendor = await Vendor.findOne({
email,
})

Interio - Full Stack 13


if (!vendor) {
throw new Error("Invalid vendor name or Sign up first
}
const isMatch = await bcrypt.compare(password, vendor.pa
if (!isMatch) {
throw new Error("Invalid Password!")
}
return vendor
}

vendorSchema.pre("save", async function (next) {


const vendor = this
if (vendor.isModified("password")) {
vendor.password = await bcrypt.hash(vendor.password, 8
}
next()
})

const Vendor: IVendorModel = (mongoose.models.Vendor as IV

export default Vendor

Shots

import mongoose from "mongoose"

mongoose.set("strictQuery", true)

const shotSchema = new mongoose.Schema(


{
title: {
type: String,
trim: true,
required: true,
},

Interio - Full Stack 14


category: {
type: String,
trim: true,
required: true,
lowercase: true,
index: true,
},
description: {
type: String,
required: true,
trim: true,
},
tags: {
type: [String],
lowercase: true,
},
images: [
{
title: String,
url: String,
},
],
owner: {
type: String,
requied: true,
ref: "Vendor",
},
like: [mongoose.Schema.Types.ObjectId],
comments: [
{
v_id: String,
comment: String,
},
],
view: {
type: Number,

Interio - Full Stack 15


default: 0,
},
},
{
timestamps: true,
}
)

const Shot = mongoose.models.Shot || mongoose.model("Shot"

export default Shot

Collection

import mongoose from "mongoose"

mongoose.set("strictQuery", true)

const collectionSchema = new mongoose.Schema(


{
cname: {
type: String,
trim: true,
required: true,
},
shots: [mongoose.Schema.Types.ObjectId],
owner: {
type: String,
requied: true,
ref: "Vendor",
},
},
{
timestamps: true,
}

Interio - Full Stack 16


)

const Collection = mongoose.models.Collection || mongoose

export default Collection

3. Server Actions

Vendor Actions

"use server"

import { cookies } from "next/headers"


import getIdByToken, { getErrorMessage } from "@/utils/hel

import SHOT from "../model/shot.model"


import VENDOR from "../model/vendor.model"
import { connectToDB } from "../mongoose"

//-------------GET ALL VENDOR-------------//


export const getAllVendors = async () => {
try {
connectToDB()
const vendor = await VENDOR.find({})
return { vendor }
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

//-------------NEW VENDOR-------------//
export const addVendor = async (body: any) => {
try {
connectToDB()

Interio - Full Stack 17


body["username"] = body.email.split("@")[0]
const vendor = new VENDOR(body)
await vendor.save()
const token = await vendor.generateAuthToken()
cookies().set("token", token)

return { name: vendor?.name, _id: vendor._id.toString(


} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

//-------------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()

Interio - Full Stack 18


const token = cookieStore.get("token")
const _id = await getIdByToken(token)

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()

const cookieStore = cookies()


const prevToken = cookieStore.get("token")
if (!prevToken) {
return { message: "Token Not Found", code: 500 }
}

const _id = await getIdByToken(prevToken)

Interio - Full Stack 19


if (!_id) {
return { message: "Token Expired", code: 500 }
}

const vendor = await VENDOR.findOne({ _id }).select("n


if (!vendor) {
return { message: "Vendor Not Found", code: 500 }
}
vendor.tokens = vendor.tokens.filter((tok) => {
return tok.token !== prevToken?.value
})

const token = await vendor.generateAuthToken()


await vendor.save()
cookies().set("token", token)
return { name: vendor?.name, _id: vendor._id.toString(
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

//-------------GET COMPLETE VENDOR-------------//


export const getVendorProfile = async () => {
try {
connectToDB()
const cookieStore = cookies()
const token = cookieStore.get("token")
if (!token) throw new Error("Token Not Found")
const _id = await getIdByToken(token)
if (!_id) throw new Error("Token Expired")
const vendor = await VENDOR.findOne({ _id }).select("-
if (!vendor) {
throw Error("NO vendor data FOUND")
}

Interio - Full Stack 20


return { vendor }
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

//-------------GET VENDOR TABS-------------//


export const getTabs = async (tab: string) => {
try {
connectToDB()
const cookieStore = cookies()
const token = cookieStore.get("token")
if (!token) return
const _id = await getIdByToken(token)
if (!_id) return
let selectWord = "shotCollections"
let tabArr: string | any[] = []
if (tab === "liked-shot") {
selectWord = "likedShot"
}
if (tab === "work") {
selectWord = "ownShot"
}

const vdr = await VENDOR.findOne({ _id }).select(selec


if (!vdr) {
throw Error("NO vendor data FOUND")
}

if (selectWord === "shotCollections") {


// const collection = await COLLECTION.findById(vend
// tabArr = collection.shots;
} else if (selectWord === "likedShot") {
tabArr = vdr.likedShot

Interio - Full Stack 21


} else {
tabArr = vdr.ownShot
}
if (tabArr.length == 0) {
return { shots: [] }
} else {
const shots = await SHOT.find({ _id: { $in: tabArr }
if (!shots) {
throw Error("Unable to get shots!")
}
return { shots }
}
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

//-------------UPDATE VENDOR-------------//

export const updateVendor = async (body: any) => {


try {
connectToDB()
const cookieStore = cookies()
const token = cookieStore.get("token")
if (!token) return
const _id = await getIdByToken(token)
if (!_id) return
const vendor: any = await VENDOR.findOne({ _id })
if (!vendor) {
throw Error("NO vendor data FOUND")
}
const updates = Object.keys(body)
updates.forEach((update) => {
vendor[update] = body[update]

Interio - Full Stack 22


})
await vendor.save()
return { vendor }
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

Shot Actions

"use server"

// import COLLECTION from '../model/collectionModel';


import { getErrorMessage } from "@/utils/helper"

import SHOT from "../model/shot.model"


import VENDOR from "../model/vendor.model"
import { connectToDB } from "../mongoose"

//-------------NEW SHOT-------------//
export const addShot = async (body: any) => {
try {
connectToDB()
const shot = await new SHOT(body)
await shot.save()

const vendor = await VENDOR.findOne({ _id: body.owner

if (!shot || !vendor) {
throw new Error("Unable to add shot!")
}

vendor.ownShot.push(shot._id)

Interio - Full Stack 23


await vendor.save()

return { shot }
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

//-------------GET ALL SHOT-------------//


export const getShot = async (type: string, limit = 10) =>
try {
connectToDB()
let where = {}
if (type) {
//@ts-ignore - typescript is not recognizing the reg
const types = type?.split(",").map((t) => new RegExp
where = { tags: { $in: types } }
}
const shots = await SHOT.find(where)
.limit(limit)
.select("title category description tags images owne
.populate("owner", "name follower following likedsho
.sort("-createdAt")
if (!shots) {
throw Error("NO SHOT FOUND")
}
return { shots }
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

Interio - Full Stack 24


//-------------GET SHOT BY ID-------------//
export const getShotById = async (id: string) => {
try {
connectToDB()
const shot = await SHOT.findById(id)
.select("title category description tags images owne
.populate("owner", "name profilePic email")

if (!shot) {
throw Error("NO SHOT FOUND")
}
return shot
} catch (error) {
return {
error: getErrorMessage(error),
}
}
}

4. Nextjs API routes

/vendor/route.ts

import { NextResponse } from "next/server"

import { getVendorProfile, updateVendor } from "@/lib/acti


import { IVendor } from "@/lib/model/types/vendor-type"

export async function GET() {


const { vendor, error } = await getVendorProfile()
if (error) {
return NextResponse.error()
}
return NextResponse.json(vendor)
}

Interio - Full Stack 25


export async function POST(request: Request) {
const body = await request.json()
const { vendor, error } = (await updateVendor(body)) as
if (error) {
return NextResponse.error()
}
return NextResponse.json(vendor)
}

/shot/route.ts

import { NextRequest, NextResponse } from "next/server"

import { addShot } from "@/lib/actions/shot.actions"

export async function POST(req: NextRequest) {


try {
const body = await req.json()
if (!body || typeof body !== "object") {
return NextResponse.error()
}

const shot = await addShot(body)


if (!shot) {
return NextResponse.error()
}

return NextResponse.json(shot)
} catch (error) {
console.error(error)
return NextResponse.error()
}
}

/shot/[shotId]/route.ts

Interio - Full Stack 26


import { NextRequest, NextResponse } from "next/server"

import { getShotById } from "@/lib/actions/shot.actions"

export async function GET(request: NextRequest, { params }


try {
const shotId = params.shotId
const shot = await getShotById(shotId)
if (!shot) return NextResponse.json({ shot: {} })

return NextResponse.json(shot)
} catch (error) {
return NextResponse.json({ error })
}
}

/uploadthing/route.ts

import { createNextRouteHandler } from "uploadthing/next"

import { ourFileRouter } from "./core"

// Export routes for Next App Router


export const { GET, POST } = createNextRouteHandler({
router: ourFileRouter,
})

// ALSO Make a separate core.ts file in the same folder

import { createUploadthing, type FileRouter } from "upload

const f = createUploadthing()

export const ourFileRouter = {

Interio - Full Stack 27


imageUploader: f({ image: { maxFileSize: "4MB" } }).onUp
return { data }
}),
} satisfies FileRouter

export type OurFileRouter = typeof ourFileRouter

Frontend(UI) Setup
Here, It has mainly 5-6 pages (routes).

├── /home
│ ├── /designs
│ │ ├── /upload
│ │ └── /[shotId]
│ ├── /profile
│ │ └── /edit

Home Page
page.tsx

import type { NextPage } from "next"


import Image from "next/image"
import Link from "next/link"
import { SHOTDATA } from "@/utils/dummy"

import { getShot } from "@/lib/actions/shot.actions"


import Hero from "@/components/hero"
import IconList from "@/components/motion/icon-list"
import RectangleCard from "@/components/rectangle-card"

const Home: NextPage = async () => {

Interio - Full Stack 28


const { shots } = await getShot("", 10)

return (
<section className="bg-dark">
<Hero />
<IconList />

{/* SHOTS SECTION */}


<h1 className="text-gray pt-16 text-center text-4xl fon
<p className="text-gray mt-4 text-center">
Upload Interior Design Shots Or Get Inspired <br /> B
</p>

<div className="padding py-16 text-2xl sm:text-4xl">


{SHOTDATA.map((_, i) => (
<Link
href={`/designs?type=${_.type}`}
className="border-gray group flex items-center j
key={i}
>
<h3 className="text-gray group-hover:text-white s
<Image
src={"/arrow.png"}
alt="arrow icon"
height={50}
width={50}
priority
className="hidden bg-primary p-2 md:group-hove
/>
</Link>
))}
</div>

{/* WEBSITE TEMPLATE IMAGES */}

<Image src={"/imac.png"} alt="music bg" height={700} wi

Interio - Full Stack 29


<Image src={"/phone.png"} alt="music bg" height={350} w

{/* SHOT LISTS */}


<h1 className="padding text-gray pt-16 text-center text
OVER <span className="text-primary">205+</span> Shots
</h1>

<div className="padding grid grid-cols-2 gap-4 md:grid-


{shots &&
shots.map((_, i) => (
<div key={i}>
<span className=" absolute m-2 rounded-2xl bg-p
<Link href={`/designs/${_._id}`}>
<Image
src={_.images[0].url}
height={250}
width={250}
alt={"man"}
className="h-40 rounded object-fill hover:c
/>
</Link>
{/* <p>{_.title}</p> */}
</div>
))}
</div>
<p className="border-gray m-auto mt-6 w-3/4 border-b pb
<Link href={"/designs"} className="rounded-md bg-prim
Check More
</Link>
</p>

<div className="my-16 grid grid-cols-5 justify-items-st


<div className="hidden lg:col-span-2 lg:block ">
<Image src={"/saly.png"} height={500} width={500} a
</div>
<div className="padding col-span-5 mr-4 sm:mr-8 lg:co

Interio - Full Stack 30


<h1 className="text-gray pt-8 text-4xl font-bold
WHAT OUR
<span className="text-primary"> USERS</span>
<br /> SAY ABOUT US
</h1>
<p className="text-gray my-6">
Our users are our strength. We do every thing pos
</p>
<div className="card text-gray my-4 p-4 ">
<p>
Interio Design is a true treasure trove for int
inspirations, from sleek modernism to cozy rust
curated galleries provide endless ideas. Whethe
for endless creative sparks!
</p>

<h6 className="mt-4 font-semibold text-primary">S


<p>Interior Designer</p>
</div>
</div>
</div>

<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>

Interio - Full Stack 31


)
}

export default Home

layout.tsx

import { TailwindIndicator } from "../components/tailwind-ind

import "./globals.css"

import { Metadata } from "next"


import { Roboto } from "next/font/google"
import ReduxProvider, { QueryProvider } from "@/utils/provide
import { NextSSRPlugin } from "@uploadthing/react/next-ssr-pl
import { extractRouterConfig } from "uploadthing/server"

import { Toaster } from "@/components/ui/sonner"

import { ourFileRouter } from "./api/uploadthing/core"


import ExtraComponents from "./extra"

const roboto = Roboto({


subsets: ["latin"],
weight: "500",
})

export const metadata: Metadata = {


title: "Interior Design | Home",
description: "Interior Design Shots, Get Inspired By Other
}

export default function RootLayout({ children }: { children:


return (
<html lang="en">

Interio - Full Stack 32


<body className={roboto.className}>
<QueryProvider>
<ReduxProvider>
{children}
<ExtraComponents />
</ReduxProvider>
</QueryProvider>
<Toaster />
<NextSSRPlugin routerConfig={extractRouterConfig(ourF
<TailwindIndicator />
</body>
</html>
)
}

Design Pages
/design/page.tsx

import { shotData } from "@/types"


import { getShot } from "@/lib/actions/shot.actions"
import ShotCard from "./shot-card"

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)

Interio - Full Stack 33


}

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>
)}
</>
)
}

// SHOT CARD COMPONENT

import React from "react"


import Image from "next/image"
import Link from "next/link"
import { shotData } from "@/types"

import { Icons } from "@/components/icons"

export default function ShotCard({ shot }: { shot: shotData }


return (
<div key={shot._id} className="mb-2 sm:mb-4">
<Link href={`/designs/${shot._id}`}>
<Image
src={shot.images[0].url}
alt="l1img"
height={350}
quality={100}

Interio - Full Stack 34


width={370}
className="duration-400 h-52 w-80 cursor-pointer ro
/>
</Link>
<div className="text-gray flex justify-between px-2 py-
<span className="text-xs md:text-sm">
<Icons.BsChatDots className="inline" /> 1.1k
</span>
<p className="flex gap-2">
<span className="text-xs md:text-sm">
<Icons.AiOutlineHeart className="inline" /> 1.1k
</span>
<span className="text-xs md:text-sm">
<Icons.AiOutlineEye className="inline" /> 1.1k
</span>
</p>
</div>
</div>
)
}

/design/[shotId]/page.tsx

import Image from "next/image"


import { shotData } from "@/types"

import { getShot, getShotById } from "@/lib/actions/shot.acti


import { Icons } from "@/components/icons"

import ShotCard from "../shot-card"

export type Props = {


params: {
shotId: string
}

Interio - Full Stack 35


}

const ShotId = async ({ params }: Props) => {


const shot = await getShotById(params.shotId)
const moreShot = await getShot("", 4)

if (!shot || shot.error) return <p className="text-center">

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>

<div className="my-8 flex items-center justify-center g


<button className="cborder bg-trans rounded px-4 py-2
<Icons.BsChatDots />

Interio - Full Stack 36


</button>
<button className="cborder bg-trans rounded px-4 py-2
<Icons.FiFolderMinus />
</button>
<button className="cborder bg-trans rounded px-4 py-2
<Icons.BsShareFill />
</button>
</div>
<p className="border-gray my-8 w-full border-[0.5px]" /

<p>{shot?.description}</p>

<h2>I am available for new projects</h2>

📪
<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>
)} */}

<div className="flex flex-col gap-y-4">


<div className="flex justify-between">

Interio - Full Stack 37


<p>Similar Shots</p>
<p className="cursor-pointer text-primary hover:und
</div>
<div className="flex gap-4 overflow-x-auto ">
{moreShot &&
moreShot.shots &&
moreShot.shots.length > 0 &&
moreShot.shots.map((shot) => <ShotCard key={shot
</div>
</div>
</>
)
}

export default ShotId

/design/upload/page.tsx

"use client"

import React, { useState } from "react"


import { useRouter } from "next/navigation"
import { TAGS } from "@/utils/dummy"
import { UploadDropzone } from "@/utils/uploadthing"
import { joiResolver } from "@hookform/resolvers/joi"
import axios from "axios"
import Joi from "joi"
import { SubmitHandler, useForm } from "react-hook-form"
import { toast } from "sonner"

import { Button } from "@/components/ui/button"


import { Checkbox } from "@/components/ui/checkbox"
import { Form, FormControl, FormField, FormItem, FormLabel, F
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"

Interio - Full Stack 38


interface IFormInput {
description: string
shotUrl: string
tags: string[]
title: string
}

const schema = Joi.object({


description: Joi.string().min(200).max(1000).required().mes
"string.empty": `Description cannot be empty`,
"any.required": `Please provide a description for your sh
"string.min": `Description should have a minimum length o
"string.max": `Description should have a maximum length o
}),
shotUrl: Joi.string().required().messages({
"string.empty": `Shot Url cannot be empty`,
"any.required": `Please provide a url for your shot. It w
}),
title: Joi.string().required().messages({
"string.empty": `Title cannot be empty`,
"any.required": `Please provide a title for your shot. It
}),
tags: Joi.array().items(Joi.string()).required().messages({
"any.required": `Please provide a tags for your shot. It
}),
})

const Upload = () => {


const router = useRouter()
const [loading, setLoading] = useState<boolean>(false)
const form = useForm<IFormInput>({
resolver: joiResolver(schema),
})

const onSubmit: SubmitHandler<IFormInput> = async (val) =>

Interio - Full Stack 39


setLoading(true)
toast.info("Uploading Shot...")
try {
const { data } = await axios.post("/api/shots", {
role: "vendor",
description: val.description,
images: {
title: "Hotel Room",
url: val.shotUrl,
},
tags: val.tags,
category: "Furniture",
title: val.title,
owner: localStorage.getItem("v_id"),
})
if (!data) {
return
}
toast.success("Shot uploaded successfully")
router.push(`/designs/${data.shot._id}`)
} catch (error) {
toast.error("Shot upload failed")
} finally {
setLoading(false)
}
}

function addTags(tag: string, tags: string[]) {


if (tags) {
return [...tags, tag]
} else {
return [tag]
}
}

return (

Interio - Full Stack 40


<>
<h1 className="mt-2 text-center">What have you been wor

<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) => {

Interio - Full Stack 41


form.setValue("shotUrl", res[0].url)
setLoading(false)
}}
onUploadBegin={() => {
setLoading(true)
}}
onUploadError={(error: Error) => {
toast.error(error.message)
}}
config={{
mode: "auto",
}}
/>
<FormField
control={form.control}
name="tags"
render={() => (
<FormItem>
<FormLabel className="mb-2 text-base">Tags</F
<div className="flex gap-2">
{TAGS.map((tag) => (
<FormField
key={tag.id}
control={form.control}
name="tags"
render={({ field }) => {
return (
<FormItem key={tag.id} className="f
<FormControl>
<Checkbox
checked={field.value?.include
onCheckedChange={(checked) =>
return checked
? field.onChange(addTags(
: field.onChange(field.va
}}

Interio - Full Stack 42


/>
</FormControl>
<FormLabel className="font-normal
</FormItem>
)
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>

<Button type="submit" className="w-full" disabled={


Upload Shot
</Button>
</form>
</Form>
</>
)
}

export default Upload

/layout.tsx

"use client"

import type { ReactElement } from "react"


import Image from "next/image"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useAppDispatch, useAppSelector } from "@/context/hoo
import { isLogin, setLogout, togglePanel, vendor as vd } from

Interio - Full Stack 43


import { EXCLUDE_PATHS, START_WITH_PATHS } from "@/utils/dumm
import clsx from "clsx"
import { toast } from "sonner"

import { vendorLogout } from "@/lib/actions/vendor.actions"


import { Icons } from "@/components/icons"

import DesignNav from "./design-nav"

type Props = {
children?: React.ReactNode
way?: "without" | "with"
}

export default function LeftSideBar({ children, way }: Props)


const dispatch = useAppDispatch()
const vendor = useAppSelector(vd)
const pathname = usePathname()
const loginStatus = useAppSelector(isLogin)

const logoutHandler = async () => {


try {
const data = await vendorLogout()
if (data.error) {
return toast.error(data.error)
}
toast.success("Logout Successfully!")
dispatch(setLogout())
} catch (error: any) {
toast.error(error?.message)
console.log("Some Error!", error)
}
}
if (pathname.startsWith("/blogs/add")) return <>{children}<

return (

Interio - Full Stack 44


<>
<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>

<div className="cursor-pointer">{loginStatus && <Ic

{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>
)}

Interio - Full Stack 45


</div>
<div className="bluebg hidden h-screen w-[220px] flex
<div className="mt-3 w-[200px]">
{/* ?.split(' ')[0] */}
<h3 className="font-sm font-medium">Hello {vendor
<p className="text-light text-xs">Check out your
</div>

<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

Interio - Full Stack 46


className={clsx(
{ "bg-secondary text-white": pathname == "/user
"flex w-[200px] items-center gap-x-2 rounded-lg
)}
>
<Icons.AiOutlineUser size={24} />
<div>
<h3 className="font-sm font-medium">104+</h3>
<p className="text-light text-xs">
Hire Designer <br /> (coming soon)
</p>
</div>
</div>
<Link
href={`/profile/chat?v_id=${vendor._id}`}
className={clsx(
{
"bg-secondary text-white": pathname == "/prof
"pointer-events-none": !loginStatus,
},
"pointer-events-none flex w-[200px] items-cente
)}
>
<Icons.BsChatDots size={24} />
<div>
{loginStatus ? (
<>
<h3 className="font-sm font-medium">{"5+"}<
<p className="text-light text-xs">Project m
</>
) : (
<p className="text-light text-sm">Login to ch
)}
</div>
</Link>
</div>

Interio - Full Stack 47


</aside>
<div
className={clsx(
{
"px-4 pt-4 text-white sm:px-8 md:pt-8 xl:px-10 "
},
"px-4 py-8 text-white sm:px-8 md:ml-[65px] md:pt-8
)}
>
{!EXCLUDE_PATHS.includes(pathname) && !START_WITH_PAT
{children}
</div>
</>
)
}

Profile Pages
profile/page.tsx

import { formatDateInDMY } from "@/utils/helper"

import { getVendorProfile } from "@/lib/actions/vendor.action


import { Icons } from "@/components/icons"

const User = async () => {


const { vendor, error } = await getVendorProfile()

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>
)

Interio - Full Stack 48


return (
<section className="space-y-6">
<div>
<h1 className="mb-1 text-xl text-primary underline de
<p className="text-sm text-foreground">{vendor?.biogr
</div>
<div>
<h1 className="mb-1 text-xl text-primary underline de
<div className="text-gray mt-2 flex flex-wrap gap-2">
{vendor?.skills.map((_, i) => (
<span className="rounded-3xl bg-secondary px-4 py
{_}
</span>
))}
</div>
</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

Interio - Full Stack 49


{formatDateInDMY(_.to) || "Now"}
</p>
</div>
))
) : (
<p className="text-foreground">No work history foun
)}

<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>
)
}

export default User

profile/edit/page.ts

Interio - Full Stack 50


"use client"

import { useEffect, useState } from "react"


import Image from "next/image"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { TAGS } from "@/utils/dummy"
import { joiResolver } from "@hookform/resolvers/joi"
import axios from "axios"
import Joi from "joi"
import { SubmitHandler, useForm } from "react-hook-form"
import { toast } from "sonner"

import { Button, buttonVariants } from "@/components/ui/butto


import { Checkbox } from "@/components/ui/checkbox"
import { Form, FormControl, FormField, FormItem, FormLabel, F
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Icons } from "@/components/icons"

interface IFormInput {
name: string
profilePic: string
email: string
contact: number
address: string
biography: string
workHistory: object
lookingFor: object
skills: string[]
}

const schema = Joi.object({


name: Joi.string().max(50).required().messages({
"string.max": "Name must be at most {#limit} characters l

Interio - Full Stack 51


"any.required": "Name is required",
}),
profilePic: Joi.string(),
contact: Joi.string().min(8).max(12).messages({
"string.min": "Contact number should be atleast 8 digitd"
"string.max": "Name must be at most 12 digits long",
}),
email: Joi.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.empty": "Email is required",
"any.required": "Email is required",
}),
address: Joi.string(),
biography: Joi.string().max(500).messages({
"string.max": "Bio must be at most {#limit} characters lo
}),
skills: Joi.array().items(Joi.string()).min(1).required().m
"any.required": "Select atleast 1 skill.",
"array.min": "Select atleast 1 skill.",
}),
// workHistory: Joi.array().items({
// title: Joi.string(),
// company: Joi.string(),
// location: Joi.string(),
// from: Joi.string(),
// to: Joi.string(),
// }),
// lookingFor: Joi.array().items({
// title: Joi.string(),
// location: Joi.string(),
// }),
})

const Edit = () => {

Interio - Full Stack 52


const [vendor, setVendor] = useState<IFormInput>()
const [loading, setLoading] = useState<boolean>(false)
const router = useRouter()
const form = useForm<IFormInput>({
resolver: joiResolver(schema),
defaultValues: vendor,
})

const getVendor = async () => {


try {
const data = await axios.get(`/api/vendor`)
if (data.status !== 200) throw new Error("Something wen
const vendor = data.data
setVendor(vendor)
if (vendor) {
form.setValue("name", vendor.name)
form.setValue("address", vendor.address)
form.setValue("email", vendor.email)
form.setValue("skills", vendor.skills)
// form.setValue("workHistory", vendor.workHistory)
// form.setValue("lookingFor", vendor.lookingFor)
// vendor.contact && form.setValue("contact", vendor
// vendor.biography && form.setValue("biography", ven
// vendor.profilePic && form.setValue("profilePic", v
}
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getVendor()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const onSubmit: SubmitHandler<IFormInput> = async (val) =>


setLoading(true)

Interio - Full Stack 53


try {
// val.skills.filter((value) => value !== "")
// remove repeated skills

val.skills = val.skills.filter((skill, index) => {


return val.skills.indexOf(skill) === index
})

const data = await axios.post(`/api/vendor`, val)


console.log(data)
toast.success("Profile updated successful")
router.push("/profile")
} catch (error) {
toast.error("Update failed")
} finally {
setLoading(false)
}
}

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({

Interio - Full Stack 54


variant: "outline",
})}
href={"/profile"}
>
Cancel Edit
</Link>
</div>

<div className="grid gap-4 md:grid-cols-3">


<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="name" {...field} defaultValu
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} defaultVal
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField

Interio - Full Stack 55


control={form.control}
name="contact"
render={({ field }) => (
<FormItem>
<FormLabel>Contact</FormLabel>
<FormControl>
<Input type="number" {...field} defaultVa
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Address</FormLabel>
<FormControl>
<Textarea {...field} defaultValue={vendor?
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="biography"
render={({ field }) => (
<FormItem>
<FormLabel>Biography</FormLabel>
<FormControl>
<Textarea {...field} defaultValue={vendor?
</FormControl>
<FormMessage />

Interio - Full Stack 56


</FormItem>
)}
/>
<FormField
control={form.control}
name="skills"
render={() => (
<FormItem>
<FormLabel className="mb-2 text-base">Skills<
<div className="flex gap-2">
{TAGS.map((tag) => (
<FormField
key={tag.id}
control={form.control}
name="skills"
render={({ field }) => {
return (
<FormItem key={tag.id} className="f
<FormControl>
<Checkbox
checked={field.value?.include
onCheckedChange={(checked) =>
return checked
? field.onChange(addSkill
: field.onChange(field.va
}}
/>
</FormControl>
<FormLabel className="font-normal
</FormItem>
)
}}
/>
))}
</div>
<FormMessage />

Interio - Full Stack 57


</FormItem>
)}
/>

<Button type="submit" className="w-full" disabled={


Save
</Button>
</form>
</Form>
</>
)
}

export default Edit

function addSkill(tag: string, tags: string[]) {


if (tags) {
return [...tags, tag]
} else {
return [tag]
}
}

profile/[tab]/page.tsx

import { shotData } from "@/types"

import { getTabs } from "@/lib/actions/vendor.actions"


import ShotCard from "@/app/(designs)/designs/shot-card"

const Profile = async ({ params }: { params: { tab: string }


const { shots, error } = (await getTabs(params.tab)) as { s

if (!shots || shots.length == 0 || error) return <div class

Interio - Full Stack 58


return (
<>
<div className="grid grid-cols-2 justify-items-center g
{/* @ts-ignore - d */}
{shots && shots.map((shot: shotData) => <ShotCard key
</div>
</>
)
}

export default Profile

/layout.tsx

"use client"

import type { ReactElement } from "react"


import Image from "next/image"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useAppDispatch } from "@/context/hook"
import { isLogin, togglePanel, vendor as vd } from "@/context
import { useSelector } from "react-redux"
import { toast } from "sonner"

import { Button, buttonVariants } from "@/components/ui/butto


import LeftSideBar from "@/app/(designs)/layout"

export default function Layout({ children }: { children: Reac


const pathname = usePathname()
const loginStatus = useSelector(isLogin)
const vendor = useSelector(vd)
const dispatch = useAppDispatch()

if (!loginStatus) {

Interio - Full Stack 59


toast.error("Signup/Signin for checking profile!")
}
return (
<LeftSideBar>
<>
<section className="flex-col">
<Image key={2} src={"/coverimg.png"} alt="cover_ima
{loginStatus && pathname !== "/profile/edit" && (
<div className="relative -mt-16 flex items-cente
<div className="flex items-center gap-4">
<Image
src={vendor?.profilePic || "/user.png"}
alt="user"
width={80}
height={80}
className="h-12 w-12 rounded-full md:h-20 m
/>
<div>
<h1>{vendor?.name}</h1>
<p className="text-gray text-xs">0 Follower
</div>
</div>
<Link
className={buttonVariants({
variant: pathname == "/profile/edit" ? "out
})}
href={pathname == "/profile/edit" ? "/profile
>
{pathname == "/profile/edit" ? "Cancel Edit"
</Link>
</div>
)}
</section>
{loginStatus ? (
<>
{!(pathname == "/profile/edit") && (

Interio - Full Stack 60


<main>
<header className="text-gray my-2 grid grid-c
<Link
href={"/profile"}
className={`${buttonVariants({
variant: "secondary",
})} ${pathname == "/profile" ? "bg-second
>
About me
</Link>
<Link
href={"/profile/work"}
className={`${buttonVariants({
variant: "secondary",
})} ${pathname == "/profile/work" ? "bg-s
>
Work
</Link>

<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>

Interio - Full Stack 61


</header>
<p className="border-gray mb-4 w-full border-
</main>
)}
{children}
</>
) : (
<div className="flex flex-col items-center justify-
<h1 className="text-gray mt-8 text-center">
Login/Signup to access messages! or Back to{" "
<Link href="/designs" className="text-primary u
Designs
</Link>{" "}
</h1>
<div className="mt-4 flex gap-4">
<Button className="mr-4 rounded bg-primary px-4
Sign up
</Button>
<Button className="rounded bg-secondary px-4 py
Sign in
</Button>
</div>
</div>
)}
</>
</LeftSideBar>
)
}

LeftSideBar - component

"use client"

import type { ReactElement } from "react"


import Image from "next/image"

Interio - Full Stack 62


import Link from "next/link"
import { usePathname } from "next/navigation"
import { useAppDispatch, useAppSelector } from "@/context/hoo
import { isLogin, setLogout, togglePanel, vendor as vd } from
import { EXCLUDE_PATHS, START_WITH_PATHS } from "@/utils/dumm
import clsx from "clsx"
import { toast } from "sonner"

import { vendorLogout } from "@/lib/actions/vendor.actions"


import { Icons } from "@/components/icons"

import DesignNav from "./design-nav"

type Props = {
children?: React.ReactNode
way?: "without" | "with"
}

export default function LeftSideBar({ children, way }: Props)


const dispatch = useAppDispatch()
const vendor = useAppSelector(vd)
const pathname = usePathname()
const loginStatus = useAppSelector(isLogin)

const logoutHandler = async () => {


try {
const data = await vendorLogout()
if (data.error) {
return toast.error(data.error)
}
toast.success("Logout Successfully!")
dispatch(setLogout())
} catch (error: any) {
toast.error(error?.message)
console.log("Some Error!", error)
}

Interio - Full Stack 63


}
if (pathname.startsWith("/blogs/add")) return <>{children}<

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>

<div className="cursor-pointer">{loginStatus && <Ic

{loginStatus && (
<Link href="/profile" className="absolute bottom-
<Image
src={vendor?.profilePic || "/user.png"}
alt="interio logo"
height={40}
width={40}

Interio - Full Stack 64


className="h-10 w-10 rounded-full object-fill
/>
</Link>
)}
</div>
<div className="bluebg hidden h-screen w-[220px] flex
<div className="mt-3 w-[200px]">
{/* ?.split(' ')[0] */}
<h3 className="font-sm font-medium">Hello {vendor
<p className="text-light text-xs">Check out your
</div>

<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)

Interio - Full Stack 65


</p>
</div>
</div>
<div
className={clsx(
{ "bg-secondary text-white": pathname == "/user
"flex w-[200px] items-center gap-x-2 rounded-lg
)}
>
<Icons.AiOutlineUser size={24} />
<div>
<h3 className="font-sm font-medium">104+</h3>
<p className="text-light text-xs">
Hire Designer <br /> (coming soon)
</p>
</div>
</div>
<Link
href={`/profile/chat?v_id=${vendor._id}`}
className={clsx(
{
"bg-secondary text-white": pathname == "/prof
"pointer-events-none": !loginStatus,
},
"pointer-events-none flex w-[200px] items-cente
)}
>
<Icons.BsChatDots size={24} />
<div>
{loginStatus ? (
<>
<h3 className="font-sm font-medium">{"5+"}<
<p className="text-light text-xs">Project m
</>
) : (
<p className="text-light text-sm">Login to ch

Interio - Full Stack 66


)}
</div>
</Link>
</div>
</aside>
<div
className={clsx(
{
"px-4 pt-4 text-white sm:px-8 md:pt-8 xl:px-10 "
},
"px-4 py-8 text-white sm:px-8 md:ml-[65px] md:pt-8
)}
>
{!EXCLUDE_PATHS.includes(pathname) && !START_WITH_PAT
{children}
</div>
</>
)
}

Context Setup
hook.ts

import { useDispatch, useSelector } from "react-redux"


import type { TypedUseSelectorHook } from "react-redux"

import type { AppDispatch, RootState } from "./store"

// Use throughout your app instead of plain `useDispatch` and


export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState>

store.ts

Interio - Full Stack 67


import { configureStore } from "@reduxjs/toolkit"

import themeReducer from "./theme"

export const store = configureStore({


reducer: {
theme: themeReducer,
},
})

// Infer the `RootState` and `AppDispatch` types from the sto


export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState
export type AppDispatch = typeof store.dispatch

theme.ts

import { createSlice } from "@reduxjs/toolkit"

import type { RootState } from "./store"

export interface themeState {


sidebar: boolean
isLogin: boolean
showPanel: boolean
panelFor: string
vendor: {
name?: string
_id?: string
token?: string
profilePic?: string
}
}

const initialState: themeState = {

Interio - Full Stack 68


sidebar: false,
isLogin: false,
showPanel: false,
panelFor: "",
vendor: {
name: "",
_id: "",
token: "",
profilePic: "",
},
}

export const themeSlice = createSlice({


name: "theme",
initialState,
reducers: {
togglePanel: (state, actions) => {
const panelType = actions.payload

if (panelType === "HIDE") {


state.showPanel = false
state.panelFor = ""
} else {
state.showPanel = true
state.panelFor = panelType
}
},

showSidebar: (state) => {


state.sidebar = true
},
hideSidebar: (state) => {
state.sidebar = false
},
setLogin: (state, actions) => {
state.isLogin = true

Interio - Full Stack 69


const { name, _id, token, profilePic } = actions?.paylo
state.vendor.name = name
state.vendor._id = _id
state.vendor.token = token
state.vendor.profilePic = profilePic

if (name && _id) {


localStorage.setItem("vendor", name)
localStorage.setItem("v_id", _id)
}
},
setLogout: (state) => {
state.isLogin = false
localStorage.removeItem("vendor")
localStorage.removeItem("v_id")
},
},
})

// Action creators are generated for each case reducer functi


export const { showSidebar, hideSidebar, setLogin, setLogout,
export const sidebar = (state: RootState) => state.theme.side
export const isLogin = (state: RootState) => state.theme.isLo
export const panelFor = (state: RootState) => state.theme.pan
export const showPanel = (state: RootState) => state.theme.sh
export const vendor = (state: RootState) => state.theme.vendo
export default themeSlice.reducer

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.

Interio - Full Stack 70


By following the guidelines and code examples provided, you can effectively
contribute to and enhance the application's features. Continued collaboration and
adherence to best practices will be key to the project's success.

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/

Interio - Full Stack 71

You might also like