fix grades

This commit is contained in:
goro 2026-06-16 19:39:12 +03:00
parent 148eaf88c9
commit 2bc0a29a0a
8 changed files with 162 additions and 46 deletions

View File

@ -2,18 +2,20 @@ import { CleanlinessIcon } from '../../Icons/CleanlinessIcon';
import { FacilityIcon } from '../../Icons/FacilityIcon'; import { FacilityIcon } from '../../Icons/FacilityIcon';
import { RestaurantIcon } from '../../Icons/RestaurantIcon'; import { RestaurantIcon } from '../../Icons/RestaurantIcon';
import { ServiceIcon } from '../../Icons/ServiceIcon'; import { ServiceIcon } from '../../Icons/ServiceIcon';
import { ComfortIcon } from '../../Icons/ComfortIcon';
import { LocationType } from '../../../types/common';
interface RatingData { interface RatingData {
score: number; score: number;
count: number; count: number;
} }
interface DetailRatings { type DetailRatings = [
environment: number; { label: string; value: number | null },
cleanliness: number; { label: string; value: number | null },
price: number; { label: string; value: number | null },
facility: number; { label: string; value: number | null },
} ];
interface RatingsCardProps<T> { interface RatingsCardProps<T> {
data: T; data: T;
@ -65,16 +67,17 @@ const RatingsCard = <T,>({
{criticDetails && ( {criticDetails && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{([ {([
{ label: 'Taste', value: criticDetails.environment, icon: <RestaurantIcon className="w-7 h-7" strokeWidth={1.1} /> }, //@ts-ignore
{ label: 'Cleanliness', value: criticDetails.cleanliness, icon: <CleanlinessIcon className="w-7 h-7" strokeWidth={1.1} /> }, { ...criticDetails[0], icon: data.detail.location_type === LocationType.Culinary ? <RestaurantIcon className="w-7 h-7" strokeWidth={1.1} /> : <ComfortIcon className="w-7 h-7" strokeWidth={1.1} /> },
{ label: 'Service', value: criticDetails.price, icon: <ServiceIcon className="w-7 h-7" strokeWidth={1.1} /> }, { ...criticDetails[1], icon: <CleanlinessIcon className="w-7 h-7" strokeWidth={1.1} /> },
{ label: 'Facilities', value: criticDetails.facility, icon: <FacilityIcon className="w-7 h-7" strokeWidth={1.1} /> }, { ...criticDetails[2], icon: <ServiceIcon className="w-7 h-7" strokeWidth={1.1} /> },
] as const).map((item) => ( { ...criticDetails[3], icon: <FacilityIcon className="w-7 h-7" strokeWidth={1.1} /> },
]).map((item) => (
<div key={item.label} className="border border-gray-600 rounded-xl px-3 py-2 flex items-center gap-3"> <div key={item.label} className="border border-gray-600 rounded-xl px-3 py-2 flex items-center gap-3">
<div className="text-tertiary flex-shrink-0">{item.icon}</div> <div className="text-tertiary flex-shrink-0">{item.icon}</div>
<div> <div>
<div className="text-tertiary text-xs">{item.label}</div> <div className="text-tertiary text-xs">{item.label}</div>
<div className="text-lg font-bold leading-none my-0.5">{item.value}</div> <div className="text-lg font-bold leading-none my-0.5">{item.value ?? '-'}</div>
<div className="text-tertiary text-xs">Based on {formatCount(criticData.count)} reviews</div> <div className="text-tertiary text-xs">Based on {formatCount(criticData.count)} reviews</div>
</div> </div>
</div> </div>
@ -99,16 +102,17 @@ const RatingsCard = <T,>({
{userDetails && ( {userDetails && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{([ {([
{ label: 'Taste', value: userDetails.environment, icon: <RestaurantIcon className="w-7 h-7" strokeWidth={1.1} /> }, //@ts-ignore
{ label: 'Cleanliness', value: userDetails.cleanliness, icon: <CleanlinessIcon className="w-7 h-7" strokeWidth={1.1} /> }, { ...userDetails[0], icon: data.detail.location_type === LocationType.Culinary ? <RestaurantIcon className="w-7 h-7" strokeWidth={1.1} /> : <ComfortIcon className="w-7 h-7" strokeWidth={1.1} /> },
{ label: 'Service', value: userDetails.price, icon: <ServiceIcon className="w-7 h-7" strokeWidth={1.1} /> }, { ...userDetails[1], icon: <CleanlinessIcon className="w-7 h-7" strokeWidth={1.1} /> },
{ label: 'Facilities', value: userDetails.facility, icon: <FacilityIcon className="w-7 h-7" strokeWidth={1.1} /> }, { ...userDetails[2], icon: <ServiceIcon className="w-7 h-7" strokeWidth={1.1} /> },
] as const).map((item) => ( { ...userDetails[3], icon: <FacilityIcon className="w-7 h-7" strokeWidth={1.1} /> },
]).map((item) => (
<div key={item.label} className="border border-gray-600 rounded-xl px-3 py-2 flex items-center gap-3"> <div key={item.label} className="border border-gray-600 rounded-xl px-3 py-2 flex items-center gap-3">
<div className="text-tertiary flex-shrink-0">{item.icon}</div> <div className="text-tertiary flex-shrink-0">{item.icon}</div>
<div> <div>
<div className="text-tertiary text-xs">{item.label}</div> <div className="text-tertiary text-xs">{item.label}</div>
<div className="text-lg font-bold leading-none my-0.5">{item.value}</div> <div className="text-lg font-bold leading-none my-0.5">{item.value ?? '-'}</div>
<div className="text-tertiary text-xs">Based on {formatCount(userData.count)} reviews</div> <div className="text-tertiary text-xs">Based on {formatCount(userData.count)} reviews</div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,43 @@
interface ComfortIconProps {
width?: number;
height?: number;
fill?: string;
className?: string;
bold?: boolean;
strokeWidth?: number;
}
export function ComfortIcon({ width = 30, height = 30, fill = 'white', className, bold = false, strokeWidth = 0.6 }: ComfortIconProps) {
const stroke = bold ? fill : 'none';
const sw = bold ? strokeWidth : 0;
return (
<svg width={width} height={height} viewBox="0 0 300 213" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4332_1873)">
<mask id="mask0_4332_1873" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-46" width="300" height="300">
<path d="M300 -46H0V254H300V-46Z" fill="white" />
</mask>
<g mask="url(#mask0_4332_1873)">
<mask id="mask1_4332_1873" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-46" width="300" height="300">
<path d="M299.707 253.707V-45.707H0.292969V253.707H299.707Z" fill="white" stroke="white" stroke-width="0.585937" />
</mask>
<g mask="url(#mask1_4332_1873)">
<path d="M40.1281 145.015L27.7941 186.711C26.7992 190.423 22.9484 192.647 19.2353 191.653C15.5228 190.657 13.2992 186.806 14.2941 183.093L23.4365 145.015H40.1281Z" stroke="white" stroke-width="11.7187" stroke-miterlimit="22.926" stroke-linecap="round" stroke-linejoin="round" />
<path d="M23.4369 109.859H276.564C286.232 109.859 294.141 117.769 294.141 127.436C294.141 137.104 286.232 145.015 276.564 145.015H23.4369C13.7689 145.015 5.85938 137.105 5.85938 127.437C5.85938 117.77 13.7689 109.859 23.4369 109.859Z" stroke="white" stroke-width="11.7187" stroke-miterlimit="22.926" stroke-linecap="round" stroke-linejoin="round" />
<path d="M259.873 145.015L272.207 186.711C273.202 190.423 277.053 192.647 280.766 191.653C284.478 190.657 286.702 186.806 285.707 183.093L276.565 145.015H259.873Z" stroke="white" stroke-width="11.7187" stroke-miterlimit="22.926" stroke-linecap="round" stroke-linejoin="round" />
<path d="M34.9277 162.594H265.072" stroke="white" stroke-width="11.7187" stroke-miterlimit="22.926" stroke-linecap="round" stroke-linejoin="round" />
<path d="M42.1875 57.1238C31.875 57.1238 23.4375 65.5613 23.4375 75.8738V109.859M276.564 109.859V34.8582C276.564 24.5451 268.127 16.1082 257.815 16.1082H168.751C158.439 16.1082 150.001 24.5451 150.001 34.8582V38.3738C150.001 48.6863 141.564 57.1238 131.251 57.1238H82.5967" stroke="white" stroke-width="11.7187" stroke-miterlimit="22.926" stroke-linecap="round" stroke-linejoin="round" />
<path d="M189.818 109.859C190.697 92.9557 203.611 64.7094 220.456 64.8922C228.369 64.9772 239.719 69.8334 252.219 82.3971C263.233 93.4672 257.678 103.811 250.463 109.859" stroke="white" stroke-width="11.7187" stroke-miterlimit="22.926" stroke-linecap="round" stroke-linejoin="round" />
<path d="M199.235 80.8274C196.772 78.0307 193.463 75.4655 190.096 74.5596C182.432 72.4965 170.268 74.1102 155.09 82.8571C141.716 90.5639 144.526 102.061 150.001 109.859" stroke="white" stroke-width="11.7187" stroke-miterlimit="22.926" stroke-linecap="round" stroke-linejoin="round" />
<path d="M59.0215 57.1238H59.1791" stroke="white" stroke-width="11.7187" stroke-miterlimit="2.613" stroke-linecap="round" stroke-linejoin="round" />
<path d="M276.425 127.436H276.563M235.547 127.435H259.591" stroke="white" stroke-width="11.7187" stroke-miterlimit="2.613" stroke-linecap="round" stroke-linejoin="round" />
</g>
</g>
</g>
<defs>
<clipPath id="clip0_4332_1873">
<rect width="300" height="213" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@ -1,19 +1,21 @@
interface FallbackImageProps { interface FallbackImageProps {
thumbnail: string thumbnail: string
locationType?: string locationType?: string
style: React.CSSProperties style?: React.CSSProperties
alt?: string alt?: string
className?: string
} }
const fallbackThumbnailSrc = 'https://otherstuff.nochill.in/public/upload/misty-forest-black-white.webp'; const fallbackThumbnailSrc = 'https://otherstuff.nochill.in/public/upload/misty-forest-black-white.webp';
const restaurantThumbnailSrc = 'https://otherstuff.nochill.in/restaorunta.webp'; const restaurantThumbnailSrc = 'https://otherstuff.nochill.in/restaorunta.webp';
const FallbackImage = ({ thumbnail, locationType, style, alt }: FallbackImageProps) => ( const FallbackImage = ({ thumbnail, locationType, style, alt, className }: FallbackImageProps) => (
<img <img
src={thumbnail} src={thumbnail}
loading={"lazy"} loading={"lazy"}
style={style} style={style}
alt={alt} alt={alt}
className={className}
onError={(e) => { onError={(e) => {
if (locationType === 'restaurant' || locationType === 'culinary') { if (locationType === 'restaurant' || locationType === 'culinary') {
e.currentTarget.src = restaurantThumbnailSrc; e.currentTarget.src = restaurantThumbnailSrc;

View File

@ -23,6 +23,7 @@ const GET_LIST_TRENDING_LOCATIONS_URI = `${BASE_URL}/locations/trending`;
const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent`; const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent`;
const GET_LOCATION_URI = `${BASE_URL}/location`; const GET_LOCATION_URI = `${BASE_URL}/location`;
const GET_LOCATION_TAGS_URI = `${BASE_URL}/location/tags`; const GET_LOCATION_TAGS_URI = `${BASE_URL}/location/tags`;
const GET_LOCATION_REVIEW_GRADES_URI = `${BASE_URL}/location/grades`;
const POST_CREATE_LOCATION = `${BASE_URL}/location`; const POST_CREATE_LOCATION = `${BASE_URL}/location`;
const POST_LOCATION_VISIT_URI = `${BASE_URL}/location`; const POST_LOCATION_VISIT_URI = `${BASE_URL}/location`;
@ -51,6 +52,7 @@ export {
GET_LOCATION_URI, GET_LOCATION_URI,
GET_SEARCH_LOCATIONS_URI, GET_SEARCH_LOCATIONS_URI,
GET_LOCATION_TAGS_URI, GET_LOCATION_TAGS_URI,
GET_LOCATION_REVIEW_GRADES_URI,
GET_IMAGES_BY_LOCATION_URI, GET_IMAGES_BY_LOCATION_URI,
GET_CURRENT_USER_REVIEW_LOCATION_URI, GET_CURRENT_USER_REVIEW_LOCATION_URI,
PATCH_USER_AVATAR, PATCH_USER_AVATAR,

View File

@ -11,8 +11,9 @@ import {
CurrentUserLocationReviews, CurrentUserLocationReviews,
} from './types'; } from './types';
import { handleApiError, useAutosizeTextArea } from '../../utils'; import { handleApiError, useAutosizeTextArea } from '../../utils';
import { getCurrentUserLocationReviewService, getImagesByLocationService, getLocationService, postReviewLocation, postReviewImages } from "../../services"; import { getCurrentUserLocationReviewService, getImagesByLocationService, getLocationService, postReviewLocation, postReviewImages, LocationReviewGrades } from "../../services";
import { recordLocationVisitService } from "../../services/locations"; import { recordLocationVisitService, getLocationReviewGradesService } from "../../services/locations";
import { LocationType } from '../../types/common';
import { DefaultSeparator, SeparatorWithAnchor, CustomInterweave, SpinnerLoading, ReviewCard, ReviewCardFull } from '../../components'; import { DefaultSeparator, SeparatorWithAnchor, CustomInterweave, SpinnerLoading, ReviewCard, ReviewCardFull } from '../../components';
import RatingsCard from '../../components/Card/RatingsCard'; import RatingsCard from '../../components/Card/RatingsCard';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -24,6 +25,7 @@ import ReactTextareaAutosize from 'react-textarea-autosize';
import { MenuIcon } from '../../../src/components/Icons/MenuIcon'; import { MenuIcon } from '../../../src/components/Icons/MenuIcon';
import ScheduleCard from '../../components/Card/ScheduleCard'; import ScheduleCard from '../../components/Card/ScheduleCard';
import FacilitiesCard from '../../components/Card/FacilitiesCard'; import FacilitiesCard from '../../components/Card/FacilitiesCard';
import FallbackImage from '../../components/Img/FallbackImage';
const SORT_TYPE = [ const SORT_TYPE = [
@ -37,6 +39,7 @@ function LocationDetail() {
const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse) const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse)
const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse()) const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse())
const [currentUserReview, setCurrentUserReview] = useState<CurrentUserLocationReviews>() const [currentUserReview, setCurrentUserReview] = useState<CurrentUserLocationReviews>()
const [reviewGrades, setReviewGrades] = useState<LocationReviewGrades | null>(null)
const [lightboxOpen, setLightboxOpen] = useState<boolean>(false) const [lightboxOpen, setLightboxOpen] = useState<boolean>(false)
const [updatePage, setUpdatePage] = useState<boolean>(true) const [updatePage, setUpdatePage] = useState<boolean>(true)
const [pageState, setPageState] = useState({ const [pageState, setPageState] = useState({
@ -127,6 +130,12 @@ function LocationDetail() {
} }
} }
async function getLocationReviewGrades(): Promise<void> {
const { data, error } = await getLocationReviewGradesService(Number(id))
if (error) return
setReviewGrades(data)
}
async function getImage(thumbnail?: String): Promise<void> { async function getImage(thumbnail?: String): Promise<void> {
try { try {
const res = await getImagesByLocationService({ page: 1, page_size: 15, location_id: Number(id) }) const res = await getImagesByLocationService({ page: 1, page_size: 15, location_id: Number(id) })
@ -261,6 +270,12 @@ function LocationDetail() {
} }
}, [updatePage, id]) }, [updatePage, id])
useEffect(() => {
if (id) {
getLocationReviewGrades()
}
}, [id])
return ( return (
<div className="content main-content mt-3"> <div className="content main-content mt-3">
<section name={"HEADER LINK"}> <section name={"HEADER LINK"}>
@ -289,11 +304,17 @@ function LocationDetail() {
className={`relative overflow-hidden cursor-zoom-in group/tile ${className}`} className={`relative overflow-hidden cursor-zoom-in group/tile ${className}`}
onClick={() => openAt(index)} onClick={() => openAt(index)}
> >
<img <FallbackImage
alt=""
thumbnail={img.src}
locationType={locationDetail.detail.location_type}
className="w-full h-full object-cover transition-all duration-300 group-hover/tile:brightness-75"
/>
{/* <img
src={img.src} src={img.src}
alt="" alt=""
className="w-full h-full object-cover transition-all duration-300 group-hover/tile:brightness-75" 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"> <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)]"> <span className="text-white text-xs font-semibold drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]">
Photo {index + 1} Photo {index + 1}
@ -406,18 +427,26 @@ function LocationDetail() {
score: Number(data.detail.user_score), score: Number(data.detail.user_score),
count: Number(data.detail.user_count) count: Number(data.detail.user_count)
})} })}
getCriticDetails={() => ({ getCriticDetails={reviewGrades ? () => {
environment: 85, const isCulinary = locationDetail.detail.location_type === LocationType.Culinary
cleanliness: 90, const g = reviewGrades.critics_grades
price: 75, return [
facility: 80 { label: isCulinary ? 'Taste' : 'Comfort', value: isCulinary ? g.avg_taste_grade : g.avg_comfort_grade },
})} { label: 'Cleanliness', value: g.avg_cleanliness_grade },
getUserDetails={() => ({ { label: 'Service', value: g.avg_service_grade },
environment: 82, { label: 'Facilities', value: g.avg_facilities_grade },
cleanliness: 88, ]
price: 70, } : undefined}
facility: 78 getUserDetails={reviewGrades ? () => {
})} const isCulinary = locationDetail.detail.location_type === LocationType.Culinary
const g = reviewGrades.users_grades
return [
{ label: isCulinary ? 'Taste' : 'Comfort', value: isCulinary ? g.avg_taste_grade : g.avg_comfort_grade },
{ label: 'Cleanliness', value: g.avg_cleanliness_grade },
{ label: 'Service', value: g.avg_service_grade },
{ label: 'Facilities', value: g.avg_facilities_grade },
]
} : undefined}
/> />
</div> </div>
<div className="w-full md:w-[320px] flex-shrink-0"> <div className="w-full md:w-[320px] flex-shrink-0">

View File

@ -3,13 +3,13 @@ import { SlideImage } from "yet-another-react-lightbox"
export interface ILocationDetail { export interface ILocationDetail {
id: number, id: number,
name: String, name: string,
address: String, address: string,
regency_name: String, regency_name: string,
province_name: String, province_name: string,
location_type: String, location_type: string,
region_name: String, region_name: string,
google_maps_link: String, google_maps_link: string,
thumbnail: string | null, thumbnail: string | null,
submitted_by: number, submitted_by: number,
critic_score: number, critic_score: number,
@ -52,7 +52,7 @@ export interface LocationReviewsResponse {
export interface LocationDetailResponse { export interface LocationDetailResponse {
detail: ILocationDetail, detail: ILocationDetail,
tags: Array<String> tags: Array<string>
users_review: Array<LocationReviewsResponse>, users_review: Array<LocationReviewsResponse>,
critics_review: Array<LocationReviewsResponse> critics_review: Array<LocationReviewsResponse>
} }

View File

@ -4,7 +4,9 @@ import {
getListTopLocationsService, getListTopLocationsService,
getLocationService, getLocationService,
getLocationTagsService, getLocationTagsService,
getLocationReviewGradesService,
} from "./locations"; } from "./locations";
export type { LocationReviewGrades, LocationReviewGradesGroup } from "./locations";
import { getImagesByLocationService } from "./images" import { getImagesByLocationService } from "./images"
import { createAccountService, loginService, logoutService } from "./auth"; import { createAccountService, loginService, logoutService } from "./auth";
import { postReviewLocation, postReviewImages, getCurrentUserLocationReviewService } from "./review"; import { postReviewLocation, postReviewImages, getCurrentUserLocationReviewService } from "./review";
@ -28,6 +30,7 @@ export {
getListTopLocationsService, getListTopLocationsService,
getLocationService, getLocationService,
getLocationTagsService, getLocationTagsService,
getLocationReviewGradesService,
getImagesByLocationService, getImagesByLocationService,
postReviewLocation, postReviewLocation,

View File

@ -7,6 +7,7 @@ import {
GET_LIST_TRENDING_LOCATIONS_URI, GET_LIST_TRENDING_LOCATIONS_URI,
GET_LOCATION_TAGS_URI, GET_LOCATION_TAGS_URI,
GET_LOCATION_URI, GET_LOCATION_URI,
GET_LOCATION_REVIEW_GRADES_URI,
GET_SEARCH_LOCATIONS_URI, GET_SEARCH_LOCATIONS_URI,
POST_CREATE_LOCATION, POST_CREATE_LOCATION,
POST_LOCATION_VISIT_URI, POST_LOCATION_VISIT_URI,
@ -173,7 +174,6 @@ async function getListTopLocationsService(params: GetListLocationsArg) {
async function getLocationService(params: GetLocationArg) { async function getLocationService(params: GetLocationArg) {
try { try {
const data = await fetchLocation(params) const data = await fetchLocation(params)
console.log(data)
return { data, error: null } return { data, error: null }
} catch (error) { } catch (error) {
throw error throw error
@ -277,6 +277,39 @@ export const recordLocationVisitService = async (locationId: number): Promise<vo
} }
} }
export interface LocationReviewGradesGroup {
avg_taste_grade: number | null
taste_grade_count: number
avg_comfort_grade: number | null
comfort_grade_count: number
avg_cleanliness_grade: number | null
cleanliness_grade_count: number
avg_service_grade: number | null
service_grade_count: number
avg_facilities_grade: number | null
facilities_grade_count: number
}
export interface LocationReviewGrades {
critics_grades: LocationReviewGradesGroup
users_grades: LocationReviewGradesGroup
}
const fetchLocationReviewGrades = async (locationId: number): Promise<LocationReviewGrades> => {
const url = `${GET_LOCATION_REVIEW_GRADES_URI}/${locationId}`
const response = await client({ method: 'GET', url })
return response.data
}
export const getLocationReviewGradesService = async (locationId: number) => {
try {
const data = await fetchLocationReviewGrades(locationId)
return { data, error: null }
} catch (error: any) {
return { data: null, error, status: error?.status }
}
}
export const getMenuItemsService = async (locationId: number) => { export const getMenuItemsService = async (locationId: number) => {
try { try {
const data = await fetchMenuItems(locationId) const data = await fetchMenuItems(locationId)