hilingriviw/src/pages/LocationDetail/index.tsx
2026-06-14 05:53:50 +03:00

886 lines
40 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Link, useNavigate, useParams } from 'react-router-dom';
import { ChangeEvent, TargetedEvent } from 'preact/compat';
import { useEffect, useRef, useState } from 'preact/hooks';
import Lightbox from 'yet-another-react-lightbox';
import useCallbackState from '../../types/state-callback';
import {
EmptyLocationDetailResponse,
LocationDetailResponse,
LocationResponse,
emptyLocationResponse,
CurrentUserLocationReviews,
} from './types';
import { handleApiError, useAutosizeTextArea } from '../../utils';
import { getCurrentUserLocationReviewService, getImagesByLocationService, getLocationService, postReviewLocation, postReviewImages } from "../../services";
import { recordLocationVisitService } from "../../services/locations";
import { DefaultSeparator, SeparatorWithAnchor, CustomInterweave, SpinnerLoading, ReviewCard, ReviewCardFull } from '../../components';
import RatingsCard from '../../components/Card/RatingsCard';
import { useSelector } from 'react-redux';
import { UserRootState } from '../../store/type';
import { DEFAULT_AVATAR_IMG } from '../../constants/default';
import { IHttpResponse } from '../../types/common';
import { ImagePlus, Globe, MapPin, Smile, Heart, MessageCircle } from 'lucide-react'
import ReactTextareaAutosize from 'react-textarea-autosize';
import { MenuIcon } from '../../../src/components/Icons/MenuIcon';
import ScheduleCard from '../../components/Card/ScheduleCard';
import FacilitiesCard from '../../components/Card/FacilitiesCard';
const SORT_TYPE = [
'highest rated',
'lowest rated',
'newest',
'oldest'
]
function LocationDetail() {
const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse)
const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse())
const [currentUserReview, setCurrentUserReview] = useState<CurrentUserLocationReviews>()
const [lightboxOpen, setLightboxOpen] = useState<boolean>(false)
const [updatePage, setUpdatePage] = useState<boolean>(true)
const [pageState, setPageState] = useState({
critic_filter_name: 'highest rated',
critic_filter_type: 0,
show_sort: false,
enable_post: true,
on_submit_loading: false,
is_score_rating_panic_msg: '',
})
const [reviewValue, setReviewValue] = useState({
review_textArea: '',
score_input: '',
title: '',
rasa: '',
suasana: '',
pelayanan: '',
kebersihan: '',
})
const [uploadedImages, setUploadedImages] = useState<{ file: File; preview: string }[]>([])
const [uploadLightboxOpen, setUploadLightboxOpen] = useState(false)
const [uploadLightboxIndex, setUploadLightboxIndex] = useState(0)
const [reviewLightboxOpen, setReviewLightboxOpen] = useState(false)
const [reviewLightboxIndex, setReviewLightboxIndex] = useState(0)
const [listReviewLightboxOpen, setListReviewLightboxOpen] = useState(false)
const [listReviewLightboxIndex, setListReviewLightboxIndex] = useState(0)
const [listReviewLightboxImages, setListReviewLightboxImages] = useState<{ src: string }[]>([])
const imageInputRef = useRef<HTMLInputElement>(null)
function handleImageSelect(e: ChangeEvent<HTMLInputElement>) {
const input = e.target as HTMLInputElement
if (!input.files) return
const newFiles = Array.from(input.files).map(file => ({
file,
preview: URL.createObjectURL(file),
}))
setUploadedImages(prev => [...prev, ...newFiles])
input.value = ''
}
function removeImage(index: number) {
setUploadedImages(prev => {
URL.revokeObjectURL(prev[index].preview)
return prev.filter((_, i) => i !== index)
})
}
function openUploadLightbox(index: number) {
setUploadLightboxIndex(index)
setUploadLightboxOpen(true)
}
const [isLoading, setIsLoading] = useState<boolean>(true)
const [currentIndex, setCurrentIndex] = useState(0);
const currentImage = locationImages?.images[currentIndex]?.src || locationDetail.detail.thumbnail || "";
const navigate = useNavigate();
const user = useSelector((state: UserRootState) => state.auth)
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useAutosizeTextArea(textAreaRef.current, reviewValue.review_textArea);
const { id } = useParams()
async function getLocationDetail(): Promise<void> {
try {
const res = await getLocationService(
{
id: Number(id)
}
)
setLocationDetail(res.data, (val) => {
if (val.detail.thumbnail) {
getImage(val.detail.thumbnail)
}
})
// Fire-and-forget visit hit. Only fires once per (browser tab × location);
// the backend additionally dedupes by client IP for 30 minutes via Redis,
// so refreshes / multiple tabs / bots cannot inflate the trending counts.
// Anything that fails here is silently swallowed inside the service.
recordLocationVisitService(Number(id))
} catch (error) {
const err = error as IHttpResponse;
if (err.status == 404) {
navigate("/")
}
alert(handleApiError(error))
}
}
async function getImage(thumbnail?: String): Promise<void> {
try {
const res = await getImagesByLocationService({ page: 1, page_size: 15, location_id: Number(id) })
res.data.images.push({ src: thumbnail })
setLocationImages(res.data)
setUpdatePage(false)
} catch (error) {
console.log(error)
}
setIsLoading(false)
}
async function getCurrentUserLocationReview(): Promise<void> {
try {
const res = await getCurrentUserLocationReviewService(Number(id))
setCurrentUserReview(res.data)
setPageState({ ...pageState, enable_post: false })
} catch (error) {
let err = error as IHttpResponse;
if (err.status == 404 || err.status == 401) {
return
}
alert(err.error.response.data.message)
}
}
function handleTextAreaChange(e: ChangeEvent<HTMLTextAreaElement>): void {
const val = e.target as HTMLTextAreaElement;
setReviewValue({
...reviewValue,
review_textArea: val.value
})
}
function handleScoreInputChange(e: ChangeEvent<HTMLInputElement>): void {
const val = e.target as HTMLInputElement;
setReviewValue({
...reviewValue,
score_input: val.value
})
setPageState({
...pageState,
is_score_rating_panic_msg: ''
})
}
function onChangeCriticsSort(e: TargetedEvent<HTMLAnchorElement>, sort_name: string, sort_type: number): void {
e.preventDefault()
setPageState({ ...pageState, show_sort: false, critic_filter_name: sort_name, critic_filter_type: sort_type })
}
async function handleSubmitReview(e: TargetedEvent<HTMLAnchorElement>) {
e.preventDefault();
setPageState({ ...pageState, on_submit_loading: true })
if (isNaN(Number(reviewValue.score_input))) {
setPageState({ ...pageState, is_score_rating_panic_msg: "SCORE MUST BE A NUMBER" })
return
}
if (Number(reviewValue.score_input) > 100) {
setPageState({ ...pageState, is_score_rating_panic_msg: "SCORE MUST BE LESS OR EQUAL THAN 100" })
return
}
if (reviewValue.score_input === '') {
setPageState({ ...pageState, is_score_rating_panic_msg: "SCORE MUSTN'T BE EMPTY" })
return
}
try {
const { data } = await postReviewLocation({
is_hided: false,
location_id: Number(id),
score: Number(reviewValue.score_input),
submitted_by: Number(user.id),
is_from_critic: user.is_critics,
comments: reviewValue.review_textArea,
title: reviewValue.title,
})
if (uploadedImages.length > 0) {
try {
await postReviewImages(data.id, uploadedImages.map(img => img.file))
} catch (imgErr) {
console.log('Image upload failed:', imgErr)
alert('Review posted, but images failed to upload. You can try again.')
}
uploadedImages.forEach(img => URL.revokeObjectURL(img.preview))
setUploadedImages([])
}
setPageState({ ...pageState, enable_post: false, on_submit_loading: false })
setReviewValue({ review_textArea: '', score_input: '', title: '', rasa: '', suasana: '', pelayanan: '', kebersihan: '' })
setCurrentUserReview({
id: data.id,
comments: data.comments,
is_from_critic: data.is_from_critic,
is_hided: data.is_hided,
location_id: data.location_id,
score: data.score,
submitted_by: data.submitted_by,
created_at: data.created_at,
updated_at: data.updated_at
})
setUpdatePage(true)
} catch (error) {
console.log(error)
alert(handleApiError(error))
setPageState({ ...pageState, on_submit_loading: false })
}
}
function handleSignInNavigation(e: TargetedEvent<HTMLAnchorElement>) {
e.preventDefault();
navigate('/login', { state: { from: `/location/${id}` } })
}
useEffect(() => {
getCurrentUserLocationReview()
}, [])
useEffect(() => {
if (updatePage || id) {
getLocationDetail()
}
}, [updatePage, id])
return (
<div className="content main-content mt-3">
<section name={"HEADER LINK"}>
<div className="text-[0.7em] pb-1.5 border-b border-[#38444d] text-tertiary whitespace-nowrap w-full overflow-x-scroll overflow-y-hidden [-webkit-overflow-scrolling:touch] [-ms-overflow-style:none] max-[380px]:px-2.5 [&::-webkit-scrollbar]:hidden [&>a:hover]:text-white [&>a:hover]:cursor-pointer">
<a className="inline-block text-white ">OVERVIEW</a>
<Link to={`/location/user_review/${id}`} className="ml-4 inline-block" >USER REVIEWS</Link>
<Link to={`/location/critic_review/${id}`} className="ml-4 inline-block" >CRITIC REVIEWS</Link>
<a className="ml-4 inline-block">COMMENTS</a>
</div>
</section>
<section className="pb-5 border-b border-[#38444d]" name={'LOCATION HEADER'}>
<div className="font-bold mt-5 text-2xl">
<h1>{locationDetail?.detail.name}</h1>
<p className={'underline flex'}><MapPin /> {locationDetail?.detail.address}</p>
</div>
{!isLoading && (() => {
const imgs = locationImages?.images ?? [];
const total = imgs.length;
if (total === 0) return null;
const openAt = (i: number) => { setCurrentIndex(i); setLightboxOpen(true); };
const ImageTile = ({ img, index, className }: { img: typeof imgs[0], index: number, className: string }) => (
<div
className={`relative overflow-hidden cursor-zoom-in group/tile ${className}`}
onClick={() => openAt(index)}
>
<img
src={img.src}
alt=""
className="w-full h-full object-cover transition-all duration-300 group-hover/tile:brightness-75"
/>
<div className="absolute bottom-0 left-0 right-0 p-2 flex justify-between items-end pointer-events-none">
<span className="text-white text-xs font-semibold drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]">
Photo {index + 1}
</span>
</div>
</div>
);
return (
<div className="mt-4 relative w-full">
{total === 1 && (
<div className="h-[420px] max-[768px]:h-[240px] rounded-lg overflow-hidden">
<ImageTile img={imgs[0]} index={0} className="h-full" />
</div>
)}
{total === 2 && (
<div className="grid grid-cols-2 gap-1 h-[420px] max-[768px]:h-[240px] rounded-lg overflow-hidden">
<ImageTile img={imgs[0]} index={0} className="" />
<ImageTile img={imgs[1]} index={1} className="" />
</div>
)}
{total === 3 && (
<div className="grid grid-cols-4 grid-rows-2 gap-1 h-[420px] max-[768px]:h-[240px] rounded-lg overflow-hidden">
<ImageTile img={imgs[0]} index={0} className="col-span-2 row-span-2" />
<ImageTile img={imgs[1]} index={1} className="col-span-2" />
<ImageTile img={imgs[2]} index={2} className="col-span-2" />
</div>
)}
{total >= 4 && (
<div className="grid grid-cols-4 grid-rows-2 gap-1 h-[420px] max-[768px]:h-[240px] rounded-lg overflow-hidden">
{/* Top-left large */}
<ImageTile img={imgs[0]} index={0} className="col-span-2 row-span-1 max-[768px]:col-span-4 max-[768px]:row-span-2" />
{/* Right full-height */}
<div
className="col-span-2 row-span-2 relative overflow-hidden cursor-zoom-in group/tile max-[768px]:hidden"
onClick={() => openAt(1)}
>
<img src={imgs[1].src} alt="" className="w-full h-full object-cover transition-all duration-300 group-hover/tile:brightness-75" />
<div className="absolute bottom-0 left-0 right-0 p-2 flex justify-between items-end pointer-events-none">
<span className="text-white text-xs font-semibold drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]">Photo 2</span>
{total > 4 && (
<span className="text-white text-xs font-semibold bg-black/50 rounded px-1.5 py-0.5">
🖼 {total.toLocaleString()}
</span>
)}
</div>
</div>
{/* Bottom-left small */}
<div
className="col-span-1 row-span-1 relative overflow-hidden cursor-zoom-in group/tile max-[768px]:hidden"
onClick={() => openAt(2)}
>
<img src={imgs[2].src} alt="" className="w-full h-full object-cover transition-all duration-300 group-hover/tile:brightness-75" />
<div className="absolute bottom-0 left-0 right-0 p-2 pointer-events-none">
<span className="text-white text-xs font-semibold drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]">Photo 3</span>
</div>
</div>
{/* Bottom-middle small — with "+more" if applicable */}
<div
className="col-span-1 row-span-1 relative overflow-hidden cursor-zoom-in group/tile max-[768px]:hidden"
onClick={() => openAt(3)}
>
<img src={imgs[3].src} alt="" className="w-full h-full object-cover transition-all duration-300 group-hover/tile:brightness-75" />
{total > 4 && (
<div className="absolute inset-0 bg-black/55 flex items-center justify-center pointer-events-none">
<span className="text-white font-bold text-2xl">+{total - 4}</span>
</div>
)}
{total <= 4 && (
<div className="absolute bottom-0 left-0 right-0 p-2 pointer-events-none">
<span className="text-white text-xs font-semibold drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]">Photo 4</span>
</div>
)}
</div>
</div>
)}
</div>
);
})()}
<div className="mt-4 flex flex-row items-center gap-5 border border-[#38444d] rounded-md px-5 py-3 text-sm w-fit mx-auto">
<a href="#" className="flex flex-row items-center gap-1.5 underline underline-offset-2 hover:text-white transition-colors">
<Globe className="w-4 h-4" />
<span>Website</span>
</a>
<a href="#" className="flex flex-row items-center gap-1.5 underline underline-offset-2 hover:text-white transition-colors">
<MenuIcon fill={'#ffffff'} className="w-5 h-5" />
<span>Menu</span>
</a>
{/* <a href="#" className="flex flex-row items-center gap-1.5 font-bold underline underline-offset-2 hover:text-white transition-colors">
<Phone className="w-4 h-4" />
<span>-</span>
</a> */}
<div className="w-px h-4 bg-[#38444d]" />
<a href="#" className="hover:text-white transition-colors underline underline-offset-2">Improve this listing</a>
</div>
<div className="flex flex-col md:flex-row gap-4 items-stretch mt-2 bg-black/[0.27] -mx-[25px] px-[25px] pb-4 md:mx-0 md:px-5 md:py-5 md:rounded-xl">
<div className="flex-1 min-w-0">
<RatingsCard
data={locationDetail}
getCriticData={(data) => ({
score: Number(data.detail.critic_score),
count: Number(data.detail.critic_count)
})}
getUserData={(data) => ({
score: Number(data.detail.user_score),
count: Number(data.detail.user_count)
})}
getCriticDetails={() => ({
environment: 85,
cleanliness: 90,
price: 75,
facility: 80
})}
getUserDetails={() => ({
environment: 82,
cleanliness: 88,
price: 70,
facility: 78
})}
/>
</div>
<div className="w-full md:w-[320px] flex-shrink-0">
<ScheduleCard
schedules={[
{ day: 'Sunday', open: '7.00 AM', close: '21.00 PM' },
{ day: 'Monday', open: '7.00 AM', close: '21.00 PM' },
{ day: 'Tuesday', open: '7.00 AM', close: '21.00 PM' },
{ day: 'Wednesday', open: '7.00 AM', close: '21.00 PM' },
{ day: 'Thursday', open: '7.00 AM', close: '21.00 PM' },
{ day: 'Friday', open: '7.00 AM', close: '21.00 PM' },
{ day: 'Saturday', open: '7.00 AM', close: '21.00 PM' },
]}
onSuggestEdit={() => console.log('suggest edit')}
/>
</div>
</div>
<FacilitiesCard
seeMoreShow
title="Facilities"
left={[
{
title: 'Main Facilities',
items: [
{ text: '5-floor main prayer hall + 1 basement (capacity 200,000+ worshippers)' },
{ text: 'Advanced high-quality sound system' },
{ text: 'Spacious courtyard for Eid prayers' },
],
},
]}
middle={[
{
title: 'Supporting Facilities',
items: [
{ text: 'Islamic library aoehantdhouantehou asntoehu sote ueaaup,' },
{ text: 'Multipurpose hall' },
{ text: 'Large and comfortable ablution area' },
],
},
]}
right={[
{
title: 'Accessibility',
items: [
{ text: 'Disability-friendly facilities' },
{ text: 'Guiding blocks for the visually impaired' },
{ text: 'Elevators with voice guidance & transparent walls' },
],
},
]}
/>
</section>
<section name={'REVIEWS SECTION'}>
<div className="mt-5 table w-full table-fixed">
<div className="text-left pr-5 max-w-[1096px] min-w-[680px] table-cell relative align-top w-full box-border">
{!user.username ?
<div className="bg-secondary text-center p-3 w-full"><a href={'#'} onClick={handleSignInNavigation} className="border-b border-white">SIGN IN</a> TO REVIEW</div>
:
<div name="REVIEW INPUT TEXTAREA" className="rounded-xl bg-black/[0.27] p-5">
{/* Avatar + Username */}
<div className="flex items-center gap-3 mb-5">
<img
loading="lazy"
src={user.avatar_picture ? user.avatar_picture.toString() : DEFAULT_AVATAR_IMG}
className="w-10 h-10 rounded-full object-cover flex-shrink-0"
/>
<span className="font-semibold">{user.username}</span>
{currentUserReview && (
<>
<span className="text-gray-600">|</span>
<div className="flex items-center gap-3 bg-secondary rounded-lg px-4 py-1.5">
<span className="text-sm tracking-wide">{user.is_critics ? "CRITIC SCORE" : "USER SCORE"}</span>
<span className="text-sm font-bold bg-primary rounded-full w-8 h-8 flex items-center justify-center">{currentUserReview.score}</span>
</div>
</>
)}
</div>
{currentUserReview ? (
<div>
{currentUserReview.created_at?.Valid && (
<span className="text-xs text-tertiary mb-4 block">
Written {new Date(String(currentUserReview.created_at.Time)).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</span>
)}
{currentUserReview.title && (
<p className="font-bold text-2xl mb-3">{currentUserReview.title}</p>
)}
{currentUserReview.images && currentUserReview.images.length > 0 && (
<div className="grid grid-cols-4 gap-1.5 mb-4 rounded-lg overflow-hidden">
{currentUserReview.images.slice(0, 4).map((img, i) => (
<div
key={img.id}
className="relative aspect-[6/3] cursor-zoom-in group/rimg overflow-hidden"
onClick={() => { setReviewLightboxIndex(i); setReviewLightboxOpen(true); }}
>
<img
src={img.src}
alt={`review-${i}`}
className="w-full h-full object-cover transition-all duration-200 group-hover/rimg:brightness-75"
/>
{i === 3 && currentUserReview.images!.length > 4 && (
<div className="absolute inset-0 bg-black/55 flex items-center justify-center pointer-events-none">
<span className="text-white font-bold text-lg">+{currentUserReview.images!.length - 4} MORE</span>
</div>
)}
</div>
))}
</div>
)}
{/* Review text — clamped with "Show Full Review" */}
<div className="mb-4">
<div className="line-clamp-5">
<CustomInterweave content={currentUserReview.comments} />
</div>
</div>
{/* Footer: likes, comments, show full review */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 text-tertiary">
<button className="flex items-center gap-1.5 hover:text-white transition-colors text-sm">
<Heart size={18} />
<span>15</span>
</button>
<button className="flex items-center gap-1.5 hover:text-white transition-colors text-sm">
<MessageCircle size={18} />
<span>5</span>
</button>
</div>
<button
onClick={() => navigate(`/location/${id}/review/${currentUserReview.id}`)}
className="border border-gray-600 rounded-lg px-4 py-2 text-sm font-semibold hover:bg-secondary transition-colors"
>
Show Full Review
</button>
</div>
</div>
) : (
<>
<div className="grid grid-cols-3 gap-3 mb-5 [&_select]:w-full [&_select]:bg-black/40 [&_select]:border [&_select]:border-gray-600 [&_select]:rounded-lg [&_select]:px-3 [&_select]:py-2 [&_select]:text-sm [&_select]:outline-none [&_select]:appearance-none [&_select]:cursor-pointer [&_select:focus]:border-gray-400">
<div>
<label className="text-xs text-tertiary block mb-1.5">Rating</label>
<select
value={reviewValue.score_input}
onChange={(e) => {
const val = (e.target as HTMLSelectElement).value;
setReviewValue({ ...reviewValue, score_input: val });
setPageState({ ...pageState, is_score_rating_panic_msg: '' });
}}
>
<option value="">-</option>
{Array.from({ length: 10 }, (_, i) => (
<option key={i + 1} value={i + 1}>{i + 1}</option>
))}
</select>
</div>
<div>
<label className="text-xs text-tertiary block mb-1.5">Rasa</label>
<select
value={reviewValue.rasa}
onChange={(e) => setReviewValue({ ...reviewValue, rasa: (e.target as HTMLSelectElement).value })}
>
<option value="">-</option>
{Array.from({ length: 10 }, (_, i) => (
<option key={i + 1} value={i + 1}>{i + 1}</option>
))}
</select>
</div>
<div>
<label className="text-xs text-tertiary block mb-1.5">Suasana</label>
<select
value={reviewValue.suasana}
onChange={(e) => setReviewValue({ ...reviewValue, suasana: (e.target as HTMLSelectElement).value })}
>
<option value="">-</option>
{Array.from({ length: 10 }, (_, i) => (
<option key={i + 1} value={i + 1}>{i + 1}</option>
))}
</select>
</div>
<div />
<div>
<label className="text-xs text-tertiary block mb-1.5">Pelayanan</label>
<select
value={reviewValue.pelayanan}
onChange={(e) => setReviewValue({ ...reviewValue, pelayanan: (e.target as HTMLSelectElement).value })}
>
<option value="">-</option>
{Array.from({ length: 10 }, (_, i) => (
<option key={i + 1} value={i + 1}>{i + 1}</option>
))}
</select>
</div>
<div>
<label className="text-xs text-tertiary block mb-1.5">Kebersihan</label>
<select
value={reviewValue.kebersihan}
onChange={(e) => setReviewValue({ ...reviewValue, kebersihan: (e.target as HTMLSelectElement).value })}
>
<option value="">-</option>
{Array.from({ length: 10 }, (_, i) => (
<option key={i + 1} value={i + 1}>{i + 1}</option>
))}
</select>
</div>
</div>
{pageState.is_score_rating_panic_msg && (
<div className="text-xs text-error mb-3">{pageState.is_score_rating_panic_msg}</div>
)}
{/* Hidden file input */}
<input
ref={imageInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={handleImageSelect}
/>
<button
onClick={() => imageInputRef.current?.click()}
className={`flex items-center gap-2 border border-gray-600 rounded-lg px-4 py-2 text-sm hover:bg-primary transition-colors text-tertiary hover:text-white ${uploadedImages.length === 0 ? 'mb-5' : ''}`}
>
<ImagePlus size={16} />
Add Images {uploadedImages.length > 0 && `(${uploadedImages.length})`}
</button>
{uploadedImages.length > 0 && (
<div className="relative mt-3 mb-5">
<div className="pointer-events-none absolute right-0 top-0 h-full w-12 bg-gradient-to-l from-black/[0.27] to-transparent z-10 rounded-r-lg" />
<div className="flex flex-row gap-2 overflow-x-auto pb-2 [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-gray-600 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb:hover]:bg-gray-400">
{uploadedImages.map((img, i) => (
<div key={i} className="relative flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden group/thumb">
{/* Click image to open lightbox */}
<img
src={img.preview}
alt={`upload-${i}`}
className="w-full h-full object-cover cursor-zoom-in"
onClick={() => openUploadLightbox(i)}
/>
{/* X button — top-right corner, always visible */}
<button
onClick={(e) => { e.stopPropagation(); removeImage(i); }}
className="absolute top-1 right-1 w-5 h-5 rounded-full bg-black/70 flex items-center justify-center text-white text-xs leading-none hover:bg-black transition-colors"
>
&times;
</button>
</div>
))}
</div>
</div>
)}
<div className="mb-3">
<label className="text-xs text-tertiary block mb-1.5">Title your review</label>
<input
type="text"
placeholder="Give us the gist of your experience"
value={reviewValue.title}
onChange={(e) => setReviewValue({ ...reviewValue, title: (e.target as HTMLInputElement).value })}
className="w-full bg-transparent border border-gray-600 rounded-lg px-3 py-2 text-sm outline-none focus:border-gray-400 transition-colors placeholder:text-quartenary"
/>
</div>
<div className="mb-4">
<label className="text-xs text-tertiary block mb-1.5">Write your review</label>
<div className="relative">
<ReactTextareaAutosize
minRows={4}
onChange={handleTextAreaChange}
ref={textAreaRef}
placeholder="Share your experience......."
value={reviewValue.review_textArea}
className="w-full bg-transparent border border-gray-600 rounded-lg px-3 py-2 text-sm outline-none focus:border-gray-400 transition-colors resize-none placeholder:text-quartenary pb-8 &::-webkit-scrollbar]:hidden [scrollbar-width:none]"
/>
<button className="absolute bottom-3 right-3 text-tertiary hover:text-white transition-colors text-base leading-none">
<Smile width={24} height={24} />
</button>
</div>
</div>
<div className="flex justify-end">
{pageState.on_submit_loading ? (
<SpinnerLoading />
) : (
pageState.enable_post && (
<a
href="#"
onClick={handleSubmitReview}
className="bg-primary border border-gray-600 rounded-lg px-8 py-2 text-sm font-semibold hover:bg-secondary transition-colors"
>
Post
</a>
)
)}
</div>
</>
)}
</div>
}
<div name={'CRTICITS REVIEW'} className="my-[50px] mx-0 text-left">
<SeparatorWithAnchor pageName={"critic's review"} pageLink='#' />
{locationDetail.critics_review.length > 0 ?
<>
{locationDetail.critics_review.map(x => {
return (
<ReviewCard
avatar={x.user_avatar}
username={x.username}
score={x.score}
// isCritic={x.is_from_critic}
title={x.title}
comment={x.comments}
images={x.images}
likes={0}
commentsCount={0}
onShowFullReview={() => navigate(`/location/${id}/review/${x.id}`)}
/>
)
})}
</>
:
<span className="text-sm italic">No Critics review to display</span>
}
</div>
<div name={'USERS REVIEW'} className="my-[50px] mx-0 text-left">
<SeparatorWithAnchor pageName={"User's review"} pageLink='#' secondLink={locationDetail.users_review.length > 0 ? '#' : ''} />
{locationDetail.users_review.length > 0 ?
<>
{locationDetail.users_review.map(x => (
<ReviewCard
avatar={x.user_avatar}
username={x.username}
score={x.score}
// isCritic={x.is_from_critic}
title={x.title}
comment={x.comments}
images={x.images}
likes={0}
commentsCount={0}
onShowFullReview={() => navigate(`/location/${id}/review/${x.id}`)}
/>
// <div key={x.id} className="py-4 border-b border-[#38444d] last:border-b-0">
// {/* Header: avatar + username + score */}
// <div className="flex items-center gap-3 mb-3">
// <img
// loading="lazy"
// className="w-10 h-10 rounded-full object-cover flex-shrink-0"
// src={x.user_avatar ?? DEFAULT_AVATAR_IMG}
// />
// <span className="font-semibold text-sm">{x.username}</span>
// <span className="text-gray-600">|</span>
// <div className="flex items-center gap-2 bg-secondary rounded-lg px-3 py-1">
// <span className="text-xs tracking-wide text-tertiary">USER SCORE</span>
// <span className="text-xs font-bold bg-primary rounded-full w-6 h-6 flex items-center justify-center">{x.score}</span>
// </div>
// </div>
// {x.title && (
// <p className="font-bold text-xl underline mb-2">{x.title}</p>
// )}
// {x.images && x.images.length > 0 && (
// <div className="flex flex-row gap-1.5 mb-3 overflow-x-auto [&::-webkit-scrollbar]:h-1 [&::-webkit-scrollbar-thumb]:bg-gray-600 [&::-webkit-scrollbar-thumb]:rounded-full">
// {x.images.slice(0, 20).map((img, i) => (
// <div
// key={img.id}
// className="relative w-16 h-16 flex-shrink-0 overflow-hidden rounded cursor-zoom-in group/limg"
// onClick={() => {
// setListReviewLightboxImages(x.images!.map(im => ({ src: im.src })))
// setListReviewLightboxIndex(i)
// setListReviewLightboxOpen(true)
// }}
// >
// <img
// src={img.src}
// alt={`review-img-${i}`}
// className="w-full h-full object-cover transition-all duration-200 group-hover/limg:brightness-75"
// />
// {i === 19 && x.images!.length > 20 && (
// <div className="absolute inset-0 bg-black/55 flex items-center justify-center pointer-events-none">
// <span className="text-white font-bold text-sm">+{x.images!.length - 20} MORE</span>
// </div>
// )}
// </div>
// ))}
// </div>
// )}
// {/* Comment */}
// <div className="text-md leading-6 break-words line-clamp-5">
// <CustomInterweave content={x.comments} />
// </div>
// </div>
))}
<div className="text-center text-sm mt-5 [&>a:hover]:bg-[#38444d] [&>a:hover]:text-white [&>a:hover]:cursor-pointer">
<a className="rounded-[15px] py-2 px-8 border border-[#d8d8d8]">
More
</a>
</div>
</>
:
<>
<span className="text-sm italic">No users review to display</span>
</>
}
</div>
<div className="mb-5">
CONTRUBITION
<DefaultSeparator />
anoeantoeh aoenthaoe aoenth aot
</div>
</div>
</div>
<div className="clear-both" />
</section>
<section>
<div className="text-center text-md pt-5 pb-5">
Added on: 28 May 1988
</div>
</section>
<Lightbox
open={lightboxOpen}
close={() => setLightboxOpen(false)}
slides={locationImages?.images}
/>
<Lightbox
open={uploadLightboxOpen}
close={() => setUploadLightboxOpen(false)}
index={uploadLightboxIndex}
slides={uploadedImages.map(img => ({ src: img.preview }))}
/>
<Lightbox
open={reviewLightboxOpen}
close={() => setReviewLightboxOpen(false)}
index={reviewLightboxIndex}
slides={currentUserReview?.images?.map(img => ({ src: img.src })) ?? []}
/>
<Lightbox
open={listReviewLightboxOpen}
close={() => setListReviewLightboxOpen(false)}
index={listReviewLightboxIndex}
slides={listReviewLightboxImages}
/>
</div>
)
}
export default LocationDetail;