Skip to content

Commit a7f5668

Browse files
committed
feat: add review feature in orders and order description page
1 parent eb3c420 commit a7f5668

File tree

9 files changed

+313
-24
lines changed

9 files changed

+313
-24
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the `Address` table. If the table is not empty, all the data it contains will be lost.
5+
6+
*/
7+
-- DropForeignKey
8+
ALTER TABLE "Address" DROP CONSTRAINT "Address_userId_fkey";
9+
10+
-- DropTable
11+
DROP TABLE "Address";
12+
13+
-- CreateTable
14+
CREATE TABLE "Review" (
15+
"id" TEXT NOT NULL,
16+
"message" TEXT NOT NULL,
17+
"rating" INTEGER NOT NULL,
18+
"productId" TEXT NOT NULL,
19+
"userId" TEXT NOT NULL,
20+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
21+
"updatedAt" TIMESTAMP(3),
22+
23+
CONSTRAINT "Review_pkey" PRIMARY KEY ("id")
24+
);
25+
26+
-- AddForeignKey
27+
ALTER TABLE "Review" ADD CONSTRAINT "Review_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
28+
29+
-- AddForeignKey
30+
ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

prisma/schema.prisma

+12-16
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ model User {
4848
product Product[]
4949
createdAt DateTime? @default(now())
5050
updatedAt DateTime? @updatedAt
51-
Address Address[]
51+
review Review[]
5252
Orders Orders[]
5353
}
5454

@@ -66,21 +66,16 @@ model VerificationToken {
6666
@@unique([identifier, token])
6767
}
6868

69-
model Address {
70-
id String @id @default(cuid())
71-
address String
72-
email String
73-
postalCode String
74-
fullName String
75-
mobileNumber String
76-
landMark String?
77-
province String
78-
city String
79-
area String
80-
userId String
81-
createdAt DateTime @default(now())
82-
updatedAt DateTime? @updatedAt
83-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
69+
model Review {
70+
id String @id @default(cuid())
71+
message String
72+
rating Int
73+
productId String
74+
userId String
75+
createdAt DateTime @default(now())
76+
updatedAt DateTime? @updatedAt
77+
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
78+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
8479
}
8580

8681
model Orders {
@@ -118,6 +113,7 @@ model Product {
118113
images String[]
119114
oldPrice Float
120115
newPrice Float
116+
review Review[]
121117
countInStock Int
122118
sold Int @default(0)
123119
slug String
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Dialog, Transition } from '@headlessui/react'
2+
import type { ReactNode } from 'react'
3+
import { Fragment } from 'react'
4+
import { AiOutlineClose } from 'react-icons/ai'
5+
6+
type AuthDialogProps = {
7+
open: boolean
8+
handleClose: () => void
9+
children: ReactNode
10+
heading?: string
11+
}
12+
13+
export function Ratingdialog({
14+
open,
15+
handleClose,
16+
children,
17+
heading,
18+
}: AuthDialogProps) {
19+
return (
20+
<Transition appear show={open} as={Fragment}>
21+
<Dialog
22+
as="div"
23+
onClose={handleClose}
24+
className="fixed inset-0 z-50 overflow-y-auto"
25+
>
26+
<div className="min-h-screen text-center">
27+
<Transition.Child
28+
as={Fragment}
29+
enter="ease-out duration-300"
30+
enterFrom="opacity-0"
31+
enterTo="opacity-100"
32+
leave="ease-in duration-200"
33+
leaveFrom="opacity-100"
34+
leaveTo="opacity-0"
35+
>
36+
<Dialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm dark:bg-white/5" />
37+
</Transition.Child>
38+
<span
39+
className="inline-block h-screen align-middle"
40+
aria-hidden="true"
41+
>
42+
&#8203;
43+
</span>
44+
<Transition.Child
45+
as={Fragment}
46+
enter="ease-out duration-300"
47+
enterFrom="opacity-0 scale-95"
48+
enterTo="opacity-100 scale-100"
49+
leave="ease-in duration-200"
50+
leaveFrom="opacity-100 scale-100"
51+
leaveTo="opacity-0 scale-95"
52+
>
53+
<Dialog.Panel className="relative my-8 inline-block w-full max-w-md overflow-hidden bg-zinc-50 p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-900 sm:rounded-md">
54+
<button
55+
onClick={handleClose}
56+
className="float-right rounded-md p-1 transition hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-600/50 dark:hover:bg-slate-800 "
57+
>
58+
<span className="sr-only">close sign in modal</span>
59+
<AiOutlineClose aria-hidden="true" className="h-5 w-5" />
60+
</button>
61+
<div className="py-8">
62+
{heading ? (
63+
<Dialog.Title
64+
as="h3"
65+
className="text-center text-lg font-medium leading-6 text-gray-900 dark:text-gray-300"
66+
>
67+
{heading}
68+
</Dialog.Title>
69+
) : null}
70+
<div className="mt-2">{children}</div>
71+
</div>
72+
</Dialog.Panel>
73+
</Transition.Child>
74+
</div>
75+
</Dialog>
76+
</Transition>
77+
)
78+
}

src/components/rating/Ratingform.tsx

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { zodResolver } from '@hookform/resolvers/zod'
2+
import { useState } from 'react'
3+
import { useForm } from 'react-hook-form'
4+
import { toast } from 'react-hot-toast'
5+
import { z } from 'zod'
6+
7+
import { classNames } from '@/utils/classNames'
8+
import { generateKey } from '@/utils/generateKey'
9+
import { trpc } from '@/utils/trpc'
10+
11+
import { Textarea } from '../common/Textarea'
12+
import { SubmitButton } from '../dashboard/common/Buttons'
13+
14+
const schema = z.object({
15+
message: z.string().min(4, 'Review must be 4 characters long.'),
16+
})
17+
type ISchema = z.infer<typeof schema>
18+
type RatingformProps = {
19+
handleClose: () => void
20+
productId: string
21+
}
22+
23+
export function Ratingform({ productId, handleClose }: RatingformProps) {
24+
const review = trpc.review.add.useMutation()
25+
const [rating, setRating] = useState(0)
26+
const [hover, setHover] = useState(0)
27+
const {
28+
register,
29+
handleSubmit,
30+
formState: { errors, isSubmitting },
31+
} = useForm<ISchema>({
32+
resolver: zodResolver(schema),
33+
})
34+
async function submit(val: ISchema) {
35+
await review.mutate(
36+
{ ...val, rating, productId },
37+
{
38+
onSuccess() {
39+
handleClose()
40+
toast.success('Rating added')
41+
},
42+
}
43+
)
44+
}
45+
return (
46+
<form onSubmit={handleSubmit((v) => submit(v))} className="space-y-4">
47+
{review.isError ? (
48+
<p className="text-red-500">{review.error.message}</p>
49+
) : null}
50+
<div>
51+
<p>Rating</p>
52+
{Array.from({ length: 5 }).map((v, i) => (
53+
<button
54+
className={classNames(
55+
'border-none outline-none bg-transparent',
56+
i + 1 <= ((rating && hover) || hover)
57+
? 'dark:text-gray-300 text-gray-800'
58+
: 'dark:text-gray-700 text-gray-300'
59+
)}
60+
type="button"
61+
key={generateKey()}
62+
onDoubleClick={() => {
63+
setRating(0)
64+
setHover(0)
65+
}}
66+
onMouseEnter={() => setHover(i + 1)}
67+
onMouseLeave={() => setHover(rating)}
68+
onClick={() => setRating(i + 1)}
69+
>
70+
<span className="text-2xl md:text-4xl">&#9733;</span>
71+
</button>
72+
))}
73+
</div>
74+
<Textarea
75+
error={errors.message}
76+
rows={5}
77+
placeholder="Review..."
78+
label="Review"
79+
{...register('message')}
80+
/>
81+
<SubmitButton disabled={isSubmitting || review.isLoading} />
82+
</form>
83+
)
84+
}

src/pages/order/[id].tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@ import type {
55
} from 'next'
66
import Image from 'next/image'
77
import Link from 'next/link'
8+
import { useState } from 'react'
89
import superjson from 'superjson'
910

1011
import { UserLayout } from '@/components/common/layout/UserLayout'
12+
import { Ratingdialog } from '@/components/rating/Ratingdialog'
13+
import { Ratingform } from '@/components/rating/Ratingform'
1114
import { createContext } from '@/server/context'
1215
import { appRouter } from '@/server/router/_app'
1316
import { trpc } from '@/utils/trpc'
1417

1518
export default function OrderDetails(
1619
props: InferGetServerSidePropsType<typeof getServerSideProps>
1720
) {
18-
console.log(props.id)
21+
const [ratingDialogOpen, setRatingDialogOpen] = useState(false)
22+
const [productId, setProductId] = useState('')
23+
function handleClose() {
24+
setProductId('')
25+
setRatingDialogOpen(false)
26+
}
27+
function handleOpen(id: string) {
28+
setProductId(id)
29+
setRatingDialogOpen(true)
30+
}
1931
const { data, isError, error, isLoading } =
2032
trpc.order.getOrderDetails.useQuery({
2133
id: props.id,
@@ -90,8 +102,9 @@ export default function OrderDetails(
90102
${item.price}
91103
</p>
92104
<button
105+
onClick={() => handleOpen(item.id)}
93106
type="button"
94-
className="rounded-md bg-pink-600 py-1 px-6 text-white shadow transition hover:bg-pink-400 dark:bg-pink-400 dark:hover:bg-pink-600"
107+
className="rounded-md bg-indigo-600 py-1 px-6 text-white shadow transition hover:bg-indigo-400 dark:bg-indigo-400 dark:hover:bg-indigo-600"
95108
>
96109
Review
97110
</button>
@@ -125,6 +138,13 @@ export default function OrderDetails(
125138
) : null}
126139
</section>
127140
</div>
141+
<Ratingdialog
142+
handleClose={handleClose}
143+
open={ratingDialogOpen}
144+
heading="Review"
145+
>
146+
<Ratingform productId={productId} handleClose={handleClose} />
147+
</Ratingdialog>
128148
</UserLayout>
129149
)
130150
}

src/pages/order/index.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import type { GetServerSidePropsContext } from 'next'
33
import Image from 'next/image'
44
import Link from 'next/link'
55
import { getSession } from 'next-auth/react'
6+
import { useState } from 'react'
67
import superjson from 'superjson'
78

89
import { UserLayout } from '@/components/common/layout/UserLayout'
10+
import { Ratingdialog } from '@/components/rating/Ratingdialog'
11+
import { Ratingform } from '@/components/rating/Ratingform'
912
import { createContext } from '@/server/context'
1013
import { appRouter } from '@/server/router/_app'
1114
import { trpc } from '@/utils/trpc'
@@ -19,8 +22,17 @@ type Item = {
1922
slug: string
2023
}
2124
export default function Order() {
25+
const [ratingDialogOpen, setRatingDialogOpen] = useState(false)
26+
const [productId, setProductId] = useState('')
27+
function handleClose() {
28+
setProductId('')
29+
setRatingDialogOpen(false)
30+
}
31+
function handleOpen(id: string) {
32+
setProductId(id)
33+
setRatingDialogOpen(true)
34+
}
2235
const { data, error, isError, isLoading } = trpc.order.getOrders.useQuery()
23-
console.log(data)
2436
return (
2537
<UserLayout>
2638
<div className="section mx-auto max-w-7xl pt-20">
@@ -91,8 +103,9 @@ export default function Order() {
91103
${itm.price}
92104
</p>
93105
<button
106+
onClick={() => handleOpen(itm.id)}
94107
type="button"
95-
className="rounded-md bg-pink-600 py-1 px-6 text-white shadow transition hover:bg-pink-400 dark:bg-pink-400 dark:hover:bg-pink-600"
108+
className="rounded-md bg-indigo-600 py-1 px-6 text-white shadow transition hover:bg-indigo-400 dark:bg-indigo-400 dark:hover:bg-indigo-600"
96109
>
97110
Review
98111
</button>
@@ -120,6 +133,13 @@ export default function Order() {
120133
</div>
121134
</section>
122135
</div>
136+
<Ratingdialog
137+
handleClose={handleClose}
138+
open={ratingDialogOpen}
139+
heading="Review"
140+
>
141+
<Ratingform productId={productId} handleClose={handleClose} />
142+
</Ratingdialog>
123143
</UserLayout>
124144
)
125145
}

src/server/router/_app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { router } from '../trpc'
22
import { authRouter } from './auth'
33
import { orderRouter } from './order'
44
import { productRouter } from './product'
5+
import { reviewRouter } from './review'
56
import { stripeRouter } from './stripe'
67

78
export const appRouter = router({
89
auth: authRouter,
910
product: productRouter,
1011
stripe: stripeRouter,
1112
order: orderRouter,
13+
review: reviewRouter,
1214
})
1315
export type AppRouter = typeof appRouter

0 commit comments

Comments
 (0)