Skip to content

Commit 67b5649

Browse files
Add reviews feature and work on design
1 parent 844253e commit 67b5649

17 files changed

Lines changed: 1629 additions & 3522 deletions

File tree

client/app/(auth)/signup/signUp.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ export default function SignupForm() {
8686
},
8787
});
8888

89-
const onSubmit = (data: SignUpFormData) => {
89+
const onSubmit = (data: SignUpFormData) => {
90+
console.log(data,"data signup")
9091
mutate(data);
9192
};
9293

@@ -198,14 +199,14 @@ export default function SignupForm() {
198199
<button
199200
type="button"
200201
onClick={() => setIsSignUp(false)}
201-
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
202+
className="px-4 py-2 borde cursor-pointer border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
202203
>
203204
Cancel
204205
</button>
205206
<button
206207
type="submit"
207208
disabled={isPending}
208-
className="px-4 py-2 bg-green-600 rounded-lg text-white hover:bg-green-700"
209+
className="px-4 py-2 bg-green-600 cursor-pointer rounded-lg text-white hover:bg-green-700"
209210
>
210211
{isPending ? "Creating..." : "Create Account"}
211212
</button>

client/app/_lib/review/review.tsx

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
'use client';
2+
3+
import ReactStars from 'react-stars'
4+
import { useEffect, useState } from 'react';
5+
6+
interface Review {
7+
id: string;
8+
rating: number;
9+
comment: string;
10+
createdAt: string;
11+
user:{
12+
email:string;
13+
name: string;
14+
}
15+
}
16+
17+
interface ReviewSectionProps {
18+
reviews: Review[];
19+
averageRating: number;
20+
userId?: string;
21+
featureId: string;
22+
token:string;
23+
onReviewSubmit: (rating: number, comment: string) => Promise<void>;
24+
}
25+
26+
export function ReviewSection({
27+
reviews,
28+
averageRating,
29+
featureId,
30+
onReviewSubmit,
31+
token
32+
}: ReviewSectionProps) {
33+
const [rating, setRating] = useState<number>(1);
34+
const [reviewText, setReviewText] = useState('');
35+
const [showReviewForm, setShowReviewForm] = useState(false);
36+
const [isSubmitting, setIsSubmitting] = useState(false);
37+
const [domLoaded,setDomLoaded] = useState(false);
38+
useEffect(() => {
39+
setDomLoaded(true);
40+
},[])
41+
console.log("|reviews", reviews)
42+
const handleSubmit = async () => {
43+
if (rating < 1 || rating > 5) return;
44+
45+
setIsSubmitting(true);
46+
try {
47+
await onReviewSubmit(Math.round(rating), reviewText);
48+
setRating(0);
49+
setReviewText('');
50+
setShowReviewForm(false);
51+
} finally {
52+
setIsSubmitting(false);
53+
}
54+
};
55+
56+
const handleUpdate = async () => {
57+
if (rating === 0) return;
58+
59+
setIsSubmitting(true);
60+
try {
61+
await onReviewSubmit(rating, reviewText);
62+
setRating(0);
63+
setReviewText('');
64+
setShowReviewForm(false);
65+
} finally {
66+
setIsSubmitting(false);
67+
}
68+
};
69+
70+
const handleRemove = async () => {
71+
if (rating === 0) return;
72+
73+
setIsSubmitting(true);
74+
try {
75+
await onReviewSubmit(rating, reviewText);
76+
setRating(0);
77+
setReviewText('');
78+
setShowReviewForm(false);
79+
} finally {
80+
setIsSubmitting(false);
81+
}
82+
};
83+
84+
const ratingChanged = (newRating:number) => {
85+
console.log(newRating)
86+
setRating(newRating)
87+
}
88+
89+
return (
90+
<div className="mt-12">
91+
<div className="flex justify-between items-center mb-6">
92+
<h2 className="text-2xl font-bold text-gray-700">Customer Reviews</h2>
93+
{token && (
94+
<button
95+
onClick={() => setShowReviewForm(!showReviewForm)}
96+
className="px-4 py-2 cursor-pointer bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
97+
>
98+
{showReviewForm ? 'Cancel' : 'Write a Review'}
99+
</button>
100+
)}
101+
</div>
102+
103+
{/* Rating Summary */}
104+
<div className="flex items-center mb-8 py-2 border-b-1 border-gray-300">
105+
<div className="flex mr-2">
106+
{domLoaded && <ReactStars
107+
edit={false}
108+
count={5}
109+
size={24}
110+
value={averageRating}
111+
color2={'#ffd700'} />}
112+
</div>
113+
<span className="text-lg text-gray-600">
114+
{averageRating.toFixed(1)} out of 5 ({reviews.length} reviews)
115+
</span>
116+
</div>
117+
118+
{/* Review Form */}
119+
{showReviewForm && (
120+
<div className="bg-gray-50 p-6 rounded-lg mb-8 shadow-sm">
121+
<h3 className="text-xl font-semibold text-gray-900 mb-4">Share Your Experience</h3>
122+
123+
<div className="mb-4">
124+
<label className="block text-sm font-medium text-gray-700 mb-2">Your Rating</label>
125+
<div className="flex space-x-1">
126+
{ domLoaded && <ReactStars
127+
count={5}
128+
value={rating}
129+
onChange={ratingChanged}
130+
size={24}
131+
color2={'#ffd700'} />}
132+
133+
</div>
134+
</div>
135+
136+
<div className="mb-4">
137+
<label htmlFor="review" className="block text-sm font-medium text-gray-700 mb-2">
138+
Your Review
139+
</label>
140+
<textarea
141+
id="review"
142+
rows={4}
143+
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none
144+
focus:ring-green-600 focus:border-green-500"
145+
value={reviewText}
146+
onChange={(e) => setReviewText(e.target.value)}
147+
placeholder="Share details about your experience..."
148+
/>
149+
</div>
150+
151+
<button
152+
type="button"
153+
onClick={handleSubmit}
154+
disabled={isSubmitting || rating === 0}
155+
className="px-4 py-2 bg-green-600 cursor-pointer text-white rounded-lg hover:bg-green-700 disabled:bg-green-300 transition-colors"
156+
>
157+
{isSubmitting ? 'Submitting...' : 'Submit Review'}
158+
</button>
159+
</div>
160+
)}
161+
162+
{/* Reviews List */}
163+
<div className="space-y-8">
164+
{reviews.length > 0 ? (
165+
reviews.map((review:any) => (
166+
<div key={review.id} className="border-b border-gray-200 pb-8 last:border-0">
167+
<div className="flex items-center mb-3">
168+
<div className="flex mr-2">
169+
{ domLoaded && <ReactStars
170+
count={5}
171+
value={review?.rating}
172+
onChange={ratingChanged}
173+
size={24}
174+
color2={'#ffd700'} />}
175+
</div>
176+
<span className="text-sm text-gray-500">
177+
{new Date(review.createdAt).toLocaleDateString('en-US', {
178+
year: 'numeric',
179+
month: 'long',
180+
day: 'numeric'
181+
})}
182+
</span>
183+
</div>
184+
185+
<p className="text-gray-800 mb-2">{review?.comment}</p>
186+
</div>
187+
))
188+
) : (
189+
<div className="text-left py-8">
190+
<p className="text-gray-500 text-lg">No reviews yet. Be the first to review!</p>
191+
</div>
192+
)}
193+
</div>
194+
</div>
195+
);
196+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// services/authAPI.ts
2+
export const addReview = async ({
3+
featureId,
4+
rating,
5+
comment,
6+
token
7+
}: {
8+
featureId: string;
9+
rating: number;
10+
comment: string;
11+
token: string;
12+
}) => {
13+
console.log(featureId,rating,comment,token)
14+
const res = await fetch("http://localhost:4000/api/createReviews", {
15+
method: "POST",
16+
headers: {
17+
"Content-Type": "application/json",
18+
Authorization: `Bearer ${token}`,
19+
},
20+
body: JSON.stringify({
21+
featureId,
22+
rating,
23+
comment
24+
}),
25+
});
26+
console.log("res==>", res);
27+
if (!res.ok) {
28+
const error = await res.json();
29+
throw new Error(error.message || "Interval server error");
30+
}
31+
32+
return res.json();
33+
};

client/app/components/layout/Header.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useContextAPI } from '../../_lib/context/contextAPI';
66

77
const Header = () => {
88

9-
const {setIsLogin,setIsSignUp,setIsProfile, setTokenState} = useContextAPI()
9+
const {setIsLogin,setIsSignUp,setIsProfile, setTokenState,token} = useContextAPI()
1010
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
1111
const dropdownRef = useRef<HTMLDivElement>(null);
1212
const router = useRouter();
@@ -89,8 +89,8 @@ const Header = () => {
8989
</button>
9090

9191
{isDropdownOpen && (
92-
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
93-
{isLoggedIn ? (
92+
<div className="absolute right-0 mt-2 w-48 cursor-pointer bg-white rounded-md shadow-lg py-1 z-50">
93+
{token ? (
9494
<>
9595
<button
9696
onClick={handleSetIsProfile}
@@ -111,14 +111,14 @@ const Header = () => {
111111
<>
112112
<button
113113
onClick={handleSignIn}
114-
className="flex items-center px-4 cursor-pointer py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
114+
className="flex items-center px-4 cursor-pointer py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
115115
>
116116
<UserPlus className="w-4 h-4 mr-2" />
117117
Sign In
118118
</button>
119119
<button
120120
onClick={handleSetIsSignUp}
121-
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
121+
className="flex items-center px-4 py-2 cursor-pointer text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
122122
>
123123
<UserPlus className="w-4 h-4 mr-2" />
124124
Sign up

client/app/placeViewDetails/page.tsx

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useQuery } from '@tanstack/react-query';
3+
import { useMutation, useQuery } from '@tanstack/react-query';
44
import { useSearchParams } from 'next/navigation';
55
import {
66
Phone, Wifi, Bike, Car, Video, User, Lock, Utensils, Drumstick, Bed, Key,
@@ -13,27 +13,31 @@ import FavoriteFunctionality from '../components/common/FavoriteFunctionality';
1313
import { useEffect } from 'react';
1414
import { useRouter } from 'next/navigation';
1515
import ErrorMessage from '../components/specialized/ErrorMessage';
16+
import Loader from '../components/specialized/Loader';
17+
import { ReviewSection } from '../_lib/review/review';
18+
import { addReview } from '../_lib/services/reviewService';
1619

1720
export default function PlaceViewDetails() {
21+
1822
const router = useRouter();
1923
const { token } = useContextAPI();
2024
const searchParams = useSearchParams();
2125
const id = searchParams.get('id');
2226
const decodedId = id ? decodeURIComponent(id) : null;
23-
24-
const { data, refetch,isError, error } = useQuery({
27+
const { data, refetch,isError,isLoading, error } = useQuery({
2528
queryKey: ['getPlaceDetails', decodedId!],
2629
queryFn: getPlaceViewDetails,
2730
enabled: !decodedId,
2831
});
32+
33+
2934
useEffect(() => {
3035
if(id){
3136
refetch()
3237
}
3338
},[id,token])
3439

35-
36-
if (isError) {
40+
if (isError) {
3741
return (
3842
<ErrorMessage
3943
message={error?.message || 'Failed to load Details Place.'}
@@ -42,9 +46,34 @@ if (isError) {
4246
)
4347
}
4448

45-
const place = data?.feature;
46-
const props = place?.properties || {};
49+
if (isLoading) return <Loader />
50+
const place = data?.feature;
51+
const props = place?.properties || {};
52+
const reviews = place?.review || [];
53+
54+
const averageRating = reviews?.length > 0
55+
? reviews.reduce((sum:Number, review:any) => sum + review.rating, 0) / reviews.length
56+
: 0;
4757

58+
const handleReviewSubmit = async(rating: number, comment: string) => {
59+
if (!decodedId || !token) return;
60+
61+
try {
62+
const response = await addReview({
63+
featureId: decodedId,
64+
rating,
65+
comment,
66+
token
67+
});
68+
if(response.ok){
69+
refetch()
70+
}
71+
} catch (error) {
72+
console.error('Failed to submit review:', error);
73+
throw error;
74+
}
75+
}
76+
console.log("data==>",data)
4877
return (
4978
<div className="font-sans text-gray-800 w-full px-6 sm:px-20">
5079
<div className="overflow-hidden">
@@ -127,7 +156,15 @@ if (isError) {
127156
</div>
128157

129158
</div>
130-
</div>
159+
</div>
160+
{/* reviews sections */}
161+
<ReviewSection
162+
reviews={reviews}
163+
averageRating={averageRating}
164+
featureId={decodedId || ''}
165+
onReviewSubmit={handleReviewSubmit}
166+
token={token}
167+
/>
131168
</div>
132169
</div>
133170
);
@@ -141,3 +178,5 @@ function FeatureItem({ icon, label }: { icon: React.ReactNode; label: string })
141178
</div>
142179
);
143180
}
181+
182+

0 commit comments

Comments
 (0)