Compare commits
2 Commits
148eaf88c9
...
f1bf0ca0b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1bf0ca0b5 | ||
|
|
2bc0a29a0a |
@ -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>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
interface DaySchedule {
|
interface DaySchedule {
|
||||||
day: 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday';
|
day: 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday';
|
||||||
open: string; // e.g. "7.00 AM"
|
open?: string;
|
||||||
close: string; // e.g. "21.00 PM"
|
close?: string;
|
||||||
closed?: boolean;
|
closed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ function getCurrentDay(): DaySchedule['day'] {
|
|||||||
function isOpenNow(schedules: DaySchedule[]): boolean {
|
function isOpenNow(schedules: DaySchedule[]): boolean {
|
||||||
const today = getCurrentDay();
|
const today = getCurrentDay();
|
||||||
const todaySchedule = schedules.find((s) => s.day === today);
|
const todaySchedule = schedules.find((s) => s.day === today);
|
||||||
if (!todaySchedule || todaySchedule.closed) return false;
|
if (!todaySchedule || todaySchedule.closed || !todaySchedule.open || !todaySchedule.close) return false;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const [openHour, openMin] = todaySchedule.open.replace(/[^0-9.]/g, '').split('.').map(Number);
|
const [openHour, openMin] = todaySchedule.open.replace(/[^0-9.]/g, '').split('.').map(Number);
|
||||||
@ -55,26 +55,30 @@ export function ScheduleCard({ schedules, onSuggestEdit }: ScheduleCardProps) {
|
|||||||
|
|
||||||
{/* <hr className="border-[#38444d] mb-4 mt-2" /> */}
|
{/* <hr className="border-[#38444d] mb-4 mt-2" /> */}
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
{schedules.length === 0 ? (
|
||||||
{DAYS.map((day) => {
|
<p className="text-sm text-tertiary italic">No data yet</p>
|
||||||
const schedule = schedules.find((s) => s.day === day);
|
) : (
|
||||||
const isToday = day === today;
|
<div className="flex flex-col gap-1">
|
||||||
|
{DAYS.map((day) => {
|
||||||
|
const schedule = schedules.find((s) => s.day === day);
|
||||||
|
const isToday = day === today;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={day}
|
key={day}
|
||||||
className={`flex items-center justify-between text-sm ${isToday ? 'font-bold text-white' : 'text-tertiary'}`}
|
className={`flex items-center justify-between text-sm ${isToday ? 'font-bold text-white' : 'text-tertiary'}`}
|
||||||
>
|
>
|
||||||
<span>{day}</span>
|
<span>{day}</span>
|
||||||
<span>
|
<span>
|
||||||
{!schedule || schedule.closed
|
{!schedule || schedule.closed
|
||||||
? 'Closed'
|
? 'Closed'
|
||||||
: `${schedule.open} - ${schedule.close}`}
|
: `${schedule.open} - ${schedule.close}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/components/Icons/ComfortIcon/index.tsx
Normal file
43
src/components/Icons/ComfortIcon/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Warung Sate Kambing",
|
"name": "Warung Sate Kambing",
|
||||||
"thumbnail": "https://fatahilaharis.files.wordpress.com/2017/07/wp-1499292117215.jpeg",
|
"thumbnail": "https://fatahilaharis.files.wordpress.com/2017/07/wp-1499292117215.jpeg",
|
||||||
|
"location_type": "culinary",
|
||||||
"location": "Bandung",
|
"location": "Bandung",
|
||||||
"critic_rating": 78,
|
"critic_rating": 78,
|
||||||
"visited": 48,
|
"visited": 48,
|
||||||
@ -15,6 +16,7 @@
|
|||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "Pantai Balekambang",
|
"name": "Pantai Balekambang",
|
||||||
"thumbnail": "https://www.nativeindonesia.com/foto/pantai-balekambang/Lokasi-Pura-Yang-Berada-Di-Atas-Pulau-Karang.jpg",
|
"thumbnail": "https://www.nativeindonesia.com/foto/pantai-balekambang/Lokasi-Pura-Yang-Berada-Di-Atas-Pulau-Karang.jpg",
|
||||||
|
"location_type": "culinary",
|
||||||
"location": "Malang",
|
"location": "Malang",
|
||||||
"critic_rating": 82,
|
"critic_rating": 82,
|
||||||
"visited": 48,
|
"visited": 48,
|
||||||
@ -26,6 +28,7 @@
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"name": "Bebek Sinjay",
|
"name": "Bebek Sinjay",
|
||||||
"thumbnail": "https://i0.wp.com/harga.web.id/wp-content/uploads/bebek-sinjay-surabaya-1.jpg?resize=680%2C300&ssl=1",
|
"thumbnail": "https://i0.wp.com/harga.web.id/wp-content/uploads/bebek-sinjay-surabaya-1.jpg?resize=680%2C300&ssl=1",
|
||||||
|
"location_type": "culinary",
|
||||||
"location": "Surabaya",
|
"location": "Surabaya",
|
||||||
"critic_rating": 70,
|
"critic_rating": 70,
|
||||||
"visited": 48,
|
"visited": 48,
|
||||||
@ -37,6 +40,7 @@
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"name": "Taman Pelangi",
|
"name": "Taman Pelangi",
|
||||||
"thumbnail": "https://tempat.org/wp-content/uploads/2016/11/57488321_255018015317182_1189210435487115990_n.jpg",
|
"thumbnail": "https://tempat.org/wp-content/uploads/2016/11/57488321_255018015317182_1189210435487115990_n.jpg",
|
||||||
|
"location_type": "culinary",
|
||||||
"location": "Surabaya",
|
"location": "Surabaya",
|
||||||
"critic_rating": 75,
|
"critic_rating": 75,
|
||||||
"visited": 48,
|
"visited": 48,
|
||||||
@ -47,6 +51,7 @@
|
|||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"thumbnail": "https://ak-d.tripcdn.com/images/1i61t22348mz2uz9cB9FF.jpg?proc=source/trip",
|
"thumbnail": "https://ak-d.tripcdn.com/images/1i61t22348mz2uz9cB9FF.jpg?proc=source/trip",
|
||||||
|
"location_type": "culinary",
|
||||||
"name": "Ragunan Zoo",
|
"name": "Ragunan Zoo",
|
||||||
"location": "Jakarta",
|
"location": "Jakarta",
|
||||||
"critic_rating": 88,
|
"critic_rating": 88,
|
||||||
@ -59,6 +64,7 @@
|
|||||||
"id": 6,
|
"id": 6,
|
||||||
"name": "Sate Lilit Bali",
|
"name": "Sate Lilit Bali",
|
||||||
"thumbnail": "https://lh3.googleusercontent.com/p/AF1QipNX3DCZxF8MOuyptvxPGfTT4-KNw0BTEqYqeled=s680-w680-h510",
|
"thumbnail": "https://lh3.googleusercontent.com/p/AF1QipNX3DCZxF8MOuyptvxPGfTT4-KNw0BTEqYqeled=s680-w680-h510",
|
||||||
|
"location_type": "culinary",
|
||||||
"location": "Denpasar",
|
"location": "Denpasar",
|
||||||
"critic_rating": 83,
|
"critic_rating": 83,
|
||||||
"visited": 48,
|
"visited": 48,
|
||||||
|
|||||||
@ -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,31 +427,31 @@ 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">
|
||||||
<ScheduleCard
|
<ScheduleCard
|
||||||
schedules={[
|
schedules={locationDetail.detail.business_hours ?? []}
|
||||||
{ 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')}
|
onSuggestEdit={() => console.log('suggest edit')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,21 +1,29 @@
|
|||||||
import { NullValueRes } from "../../types/common"
|
import { NullValueRes } from "../../types/common"
|
||||||
import { SlideImage } from "yet-another-react-lightbox"
|
import { SlideImage } from "yet-another-react-lightbox"
|
||||||
|
|
||||||
|
export interface BusinessHourEntry {
|
||||||
|
day: 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday';
|
||||||
|
open?: string;
|
||||||
|
close?: string;
|
||||||
|
closed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
critic_count: number,
|
critic_count: number,
|
||||||
user_score: number,
|
user_score: number,
|
||||||
user_count: number
|
user_count: number,
|
||||||
|
business_hours: BusinessHourEntry[] | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyLocationDetail(): ILocationDetail {
|
export function emptyLocationDetail(): ILocationDetail {
|
||||||
@ -34,6 +42,7 @@ export function emptyLocationDetail(): ILocationDetail {
|
|||||||
critic_count: 0,
|
critic_count: 0,
|
||||||
user_score: 0,
|
user_score: 0,
|
||||||
user_count: 0,
|
user_count: 0,
|
||||||
|
business_hours: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +61,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>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user