added bunch of cards and adjust new 7tv emote
This commit is contained in:
parent
a6d3fd5098
commit
0f2628928b
155
src/components/Card/FacilitiesCard/index.tsx
Normal file
155
src/components/Card/FacilitiesCard/index.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, Tab, TabGroup, TabList, TabPanel, TabPanels, Transition, TransitionChild } from '@headlessui/react';
|
||||||
|
|
||||||
|
export interface FacilityItem {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FacilityCategory {
|
||||||
|
title: string;
|
||||||
|
items: FacilityItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FacilitiesCardProps {
|
||||||
|
title?: string;
|
||||||
|
left: FacilityCategory[];
|
||||||
|
middle: FacilityCategory[];
|
||||||
|
right: FacilityCategory[];
|
||||||
|
seeMoreShow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CategorySection({ category }: { category: FacilityCategory }) {
|
||||||
|
return (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="text-[#C74F28] underline font-semibold mb-3">{category.title}</h4>
|
||||||
|
<ol className="list-decimal list-outside pl-5 flex flex-col gap-2.5">
|
||||||
|
{category.items.map((item, i) => (
|
||||||
|
<li key={i} className="text-md">
|
||||||
|
{item.text}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FacilitiesCard({ title = 'Facilities & Amenities', left, middle, right, seeMoreShow }: FacilitiesCardProps) {
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// Flatten all categories from the 3 columns into a single ordered list for tabs
|
||||||
|
const allCategories = [...left, ...middle, ...right];
|
||||||
|
|
||||||
|
function handleSeeMore() {
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full mt-4 bg-black/[0.27] p-6 rounded-xl">
|
||||||
|
<h3 className="text-center font-bold text-2xl mb-6">{title}</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className={'border-r-2 border-x-gray-700'}>
|
||||||
|
{left.map((cat, i) => <CategorySection key={i} category={cat} />)}
|
||||||
|
</div>
|
||||||
|
<div className={'border-r-2 border-x-gray-700'}>
|
||||||
|
{middle.map((cat, i) => <CategorySection key={i} category={cat} />)}
|
||||||
|
</div>
|
||||||
|
<div className={''}>
|
||||||
|
{right.map((cat, i) => <CategorySection key={i} category={cat} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{seeMoreShow !== undefined && (
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
<button
|
||||||
|
onClick={handleSeeMore}
|
||||||
|
className="text-sm text-tertiary underline underline-offset-2 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
See more
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dialog */}
|
||||||
|
<Transition show={dialogOpen}>
|
||||||
|
<Dialog onClose={() => setDialogOpen(false)} className="relative z-50">
|
||||||
|
|
||||||
|
{/* Backdrop */}
|
||||||
|
<TransitionChild
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black/70" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
{/* Panel container */}
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<TransitionChild
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<DialogPanel className="bg-[#1a1a1a] text-white rounded-2xl w-full max-w-3xl max-h-[85vh] flex flex-col overflow-hidden relative">
|
||||||
|
|
||||||
|
{/* Close button — top right */}
|
||||||
|
<button
|
||||||
|
onClick={() => setDialogOpen(false)}
|
||||||
|
className="absolute top-5 right-5 text-tertiary hover:text-white transition-colors text-2xl leading-none w-9 h-9 flex items-center justify-center rounded-full hover:bg-white/10 z-10"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-8 pt-8 pb-0 flex-shrink-0">
|
||||||
|
<DialogTitle className="text-4xl font-bold mb-5">{title}</DialogTitle>
|
||||||
|
|
||||||
|
{/* Pill tabs */}
|
||||||
|
<TabGroup>
|
||||||
|
<TabList className="flex flex-row gap-1 bg-[#2a2a2a] rounded-xl p-1 w-fit overflow-x-auto [-webkit-overflow-scrolling:touch] [&::-webkit-scrollbar]:hidden">
|
||||||
|
{allCategories.map((cat, i) => (
|
||||||
|
<Tab
|
||||||
|
key={i}
|
||||||
|
className="whitespace-nowrap px-5 py-2 text-sm font-medium text-tertiary rounded-lg transition-colors outline-none
|
||||||
|
data-[selected]:bg-black data-[selected]:text-white
|
||||||
|
hover:text-white"
|
||||||
|
>
|
||||||
|
{cat.title}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
{/* Tab panels */}
|
||||||
|
<TabPanels className="overflow-y-auto py-6 flex-1">
|
||||||
|
{allCategories.map((cat, i) => (
|
||||||
|
<TabPanel key={i}>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-12 gap-y-4">
|
||||||
|
{cat.items.map((item, j) => (
|
||||||
|
<div key={j} className="flex items-start gap-3">
|
||||||
|
<span className="text-sm leading-relaxed">{item.text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
))}
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FacilitiesCard;
|
||||||
@ -1,3 +1,8 @@
|
|||||||
|
import { CleanlinessIcon } from '../../Icons/CleanlinessIcon';
|
||||||
|
import { FacilityIcon } from '../../Icons/FacilityIcon';
|
||||||
|
import { RestaurantIcon } from '../../Icons/RestaurantIcon';
|
||||||
|
import { ServiceIcon } from '../../Icons/ServiceIcon';
|
||||||
|
|
||||||
interface RatingData {
|
interface RatingData {
|
||||||
score: number;
|
score: number;
|
||||||
count: number;
|
count: number;
|
||||||
@ -41,118 +46,78 @@ const RatingsCard = <T,>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 mt-2 items-center">
|
<div className="flex flex-col gap-1 items-center">
|
||||||
{/* Critics Score Section */}
|
<div className="w-full mt-2 flex flex-col md:flex-row gap-4">
|
||||||
<div className="flex gap-2 max-[768px]:flex-col max-[768px]:w-full">
|
|
||||||
<div className="p-4 bg-secondary rounded-lg w-[180px] max-[768px]:w-full">
|
{/* Critics column */}
|
||||||
<div className="font-bold text-xs mb-2 text-center">CRITICS SCORE</div>
|
<div className="flex-1 flex flex-col gap-3">
|
||||||
<div className="text-4xl text-center my-2">
|
<div className="bg-secondary rounded-xl px-4 py-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-base font-bold tracking-wide">CRITICS SCORE</div>
|
||||||
|
{criticData.count !== 0 && (
|
||||||
|
<div className="text-tertiary mt-0.5 text-xs">Based on {formatCount(criticData.count)} reviews</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-11 h-11 rounded-full bg-black flex items-center justify-center text-lg font-bold flex-shrink-0">
|
||||||
{calculateScore(criticData.score, criticData.count)}
|
{calculateScore(criticData.score, criticData.count)}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1 w-20 bg-[#72767d] mx-auto mb-2">
|
|
||||||
<div
|
|
||||||
className="h-1 bg-brand-green"
|
|
||||||
style={{ width: `${criticData.count !== 0 ? criticData.score : 0}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{criticData.count !== 0 && (
|
|
||||||
<div className="text-sm text-center">
|
|
||||||
Based on {formatCount(criticData.count)} reviews
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Critics Detail Cards */}
|
|
||||||
{criticDetails && (
|
{criticDetails && (
|
||||||
<div className="flex gap-2 max-[768px]:w-full max-[768px]:justify-between">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="p-3 bg-secondary rounded-lg w-[120px] flex flex-col max-[768px]:flex-1">
|
{([
|
||||||
<div className="font-bold text-xs mb-1 text-center">ENVIRONMENT</div>
|
{ label: 'Taste', value: criticDetails.environment, icon: <RestaurantIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="flex-1 flex flex-col items-center justify-center">
|
{ label: 'Cleanliness', value: criticDetails.cleanliness, icon: <CleanlinessIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="text-2xl text-center my-1">{criticDetails.environment}</div>
|
{ label: 'Service', value: criticDetails.price, icon: <ServiceIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="h-1 w-16 bg-[#72767d]">
|
{ label: 'Facilities', value: criticDetails.facility, icon: <FacilityIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="h-1 bg-green" style={{ width: `${criticDetails.environment}%` }} />
|
] as const).map((item) => (
|
||||||
</div>
|
<div key={item.label} className="border border-gray-600 rounded-xl px-3 py-2 flex items-center gap-3">
|
||||||
</div>
|
<div className="text-tertiary flex-shrink-0">{item.icon}</div>
|
||||||
</div>
|
<div>
|
||||||
|
<div className="text-tertiary text-xs">{item.label}</div>
|
||||||
<div className="p-3 bg-secondary rounded-lg w-[120px] flex flex-col max-[768px]:flex-1">
|
<div className="text-lg font-bold leading-none my-0.5">{item.value}</div>
|
||||||
<div className="font-bold text-xs mb-1 text-center">PRICE</div>
|
<div className="text-tertiary text-xs">Based on {formatCount(criticData.count)} reviews</div>
|
||||||
<div className="flex-1 flex flex-col items-center justify-center">
|
|
||||||
<div className="text-2xl text-center my-1">{criticDetails.price}</div>
|
|
||||||
<div className="h-1 w-16 bg-[#72767d]">
|
|
||||||
<div className="h-1 bg-green" style={{ width: `${criticDetails.price}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 bg-secondary rounded-lg w-[120px] flex flex-col max-[768px]:flex-1">
|
|
||||||
<div className="font-bold text-xs mb-1 text-center">FACILITY</div>
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center">
|
|
||||||
<div className="text-2xl text-center my-1">{criticDetails.facility}</div>
|
|
||||||
<div className="h-1 w-16 bg-[#72767d]">
|
|
||||||
<div className="h-1 bg-green" style={{ width: `${criticDetails.facility}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Users Score Section */}
|
{/* Users column */}
|
||||||
<div className="flex gap-2 max-[768px]:flex-col max-[768px]:w-full">
|
<div className="flex-1 flex flex-col gap-3">
|
||||||
<div className="p-4 bg-secondary rounded-lg w-[180px] max-[768px]:w-full">
|
<div className="bg-secondary rounded-xl px-4 py-3 flex items-center justify-between">
|
||||||
<div className="font-bold text-xs mb-2 text-center">USERS SCORE</div>
|
<div>
|
||||||
<div className="text-4xl text-center my-2">
|
<div className="text-base font-bold tracking-wide">USERS SCORE</div>
|
||||||
|
{userData.count !== 0 && (
|
||||||
|
<div className="text-tertiary mt-0.5 text-xs">Based on {formatCount(userData.count)} reviews</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-11 h-11 rounded-full bg-black flex items-center justify-center text-lg font-bold flex-shrink-0">
|
||||||
{calculateScore(userData.score, userData.count)}
|
{calculateScore(userData.score, userData.count)}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1 w-20 bg-[#72767d] mx-auto mb-2">
|
|
||||||
<div
|
|
||||||
className="h-1 bg-brand-green"
|
|
||||||
style={{ width: `${userData.count !== 0 ? userData.score / userData.count : 0}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{userData.count !== 0 && (
|
|
||||||
<div className="text-sm text-center">
|
|
||||||
Based on {formatCount(userData.count)} reviews
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Users Detail Cards */}
|
|
||||||
{userDetails && (
|
{userDetails && (
|
||||||
<div className="flex gap-2 max-[768px]:w-full max-[768px]:justify-between">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="p-3 bg-secondary rounded-lg w-[120px] flex flex-col max-[768px]:flex-1">
|
{([
|
||||||
<div className="font-bold text-xs mb-1 text-center">ENVIRONMENT</div>
|
{ label: 'Taste', value: userDetails.environment, icon: <RestaurantIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="flex-1 flex flex-col items-center justify-center">
|
{ label: 'Cleanliness', value: userDetails.cleanliness, icon: <CleanlinessIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="text-2xl text-center my-1">{userDetails.environment}</div>
|
{ label: 'Service', value: userDetails.price, icon: <ServiceIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="h-1 w-16 bg-[#72767d]">
|
{ label: 'Facilities', value: userDetails.facility, icon: <FacilityIcon className="w-7 h-7" strokeWidth={1.1} /> },
|
||||||
<div className="h-1 bg-green" style={{ width: `${userDetails.environment}%` }} />
|
] as const).map((item) => (
|
||||||
</div>
|
<div key={item.label} className="border border-gray-600 rounded-xl px-3 py-2 flex items-center gap-3">
|
||||||
</div>
|
<div className="text-tertiary flex-shrink-0">{item.icon}</div>
|
||||||
</div>
|
<div>
|
||||||
|
<div className="text-tertiary text-xs">{item.label}</div>
|
||||||
<div className="p-3 bg-secondary rounded-lg w-[120px] flex flex-col max-[768px]:flex-1">
|
<div className="text-lg font-bold leading-none my-0.5">{item.value}</div>
|
||||||
<div className="font-bold text-xs mb-1 text-center">PRICE</div>
|
<div className="text-tertiary text-xs">Based on {formatCount(userData.count)} reviews</div>
|
||||||
<div className="flex-1 flex flex-col items-center justify-center">
|
|
||||||
<div className="text-2xl text-center my-1">{userDetails.price}</div>
|
|
||||||
<div className="h-1 w-16 bg-[#72767d]">
|
|
||||||
<div className="h-1 bg-green" style={{ width: `${userDetails.price}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 bg-secondary rounded-lg w-[120px] flex flex-col max-[768px]:flex-1">
|
|
||||||
<div className="font-bold text-xs mb-1 text-center">FACILITY</div>
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center">
|
|
||||||
<div className="text-2xl text-center my-1">{userDetails.facility}</div>
|
|
||||||
<div className="h-1 w-16 bg-[#72767d]">
|
|
||||||
<div className="h-1 bg-green" style={{ width: `${userDetails.facility}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
79
src/components/Card/RatingsCardRow/index.tsx
Normal file
79
src/components/Card/RatingsCardRow/index.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { CleanlinessIcon } from '../../Icons/CleanlinessIcon';
|
||||||
|
import { FacilityIcon } from '../../Icons/FacilityIcon';
|
||||||
|
import { RestaurantIcon } from '../../Icons/RestaurantIcon';
|
||||||
|
import { ServiceIcon } from '../../Icons/ServiceIcon';
|
||||||
|
|
||||||
|
interface RatingData {
|
||||||
|
score: number;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DetailRatings {
|
||||||
|
environment: number;
|
||||||
|
cleanliness: number;
|
||||||
|
price: number;
|
||||||
|
facility: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RatingsCardRowProps<T> {
|
||||||
|
data: T;
|
||||||
|
title: string;
|
||||||
|
getRatingData: (data: T) => RatingData;
|
||||||
|
getDetails?: (data: T) => DetailRatings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RatingsCardRow = <T,>({
|
||||||
|
data,
|
||||||
|
title,
|
||||||
|
getRatingData,
|
||||||
|
getDetails,
|
||||||
|
}: RatingsCardRowProps<T>) => {
|
||||||
|
const ratingData = getRatingData(data);
|
||||||
|
const details = getDetails?.(data);
|
||||||
|
|
||||||
|
const formatCount = (count: number): string | number =>
|
||||||
|
count >= 1000 ? `${(count / 1000).toFixed(1).replace(/\.0$/, '')}k` : count;
|
||||||
|
|
||||||
|
const calculateScore = (score: number, count: number): string | number =>
|
||||||
|
count !== 0 ? Math.floor(score / count) : 'NR';
|
||||||
|
|
||||||
|
const score = calculateScore(ratingData.score, ratingData.count);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row gap-3 items-stretch">
|
||||||
|
<div className="bg-secondary rounded-xl px-5 py-5 flex-shrink-0 md:w-[320px] flex flex-col gap-2 self-center">
|
||||||
|
<div className="flex flex-row items-center justify-between gap-3">
|
||||||
|
<div className="text-2xl font-bold tracking-wide leading-tight">{title}</div>
|
||||||
|
<div className="w-16 h-16 rounded-full bg-black flex items-center justify-center text-3xl font-bold flex-shrink-0">
|
||||||
|
{score}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ratingData.count !== 0 && (
|
||||||
|
<div className="text-tertiary text-sm">Based on {formatCount(ratingData.count)} reviews</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<div className="grid grid-cols-2 gap-3 flex-1">
|
||||||
|
{([
|
||||||
|
{ label: 'Rasa', value: details.environment, icon: <RestaurantIcon className="w-10 h-10" strokeWidth={1.1} /> },
|
||||||
|
{ label: 'Kebersihan', value: details.cleanliness, icon: <CleanlinessIcon className="w-10 h-10" strokeWidth={1.1} /> },
|
||||||
|
{ label: 'Pelayanan', value: details.price, icon: <ServiceIcon className="w-10 h-10" strokeWidth={1.1} /> },
|
||||||
|
{ label: 'Fasilitas', value: details.facility, icon: <FacilityIcon className="w-10 h-10" strokeWidth={1.1} /> },
|
||||||
|
] as const).map((item) => (
|
||||||
|
<div key={item.label} className="border border-gray-700 rounded-xl px-4 py-3 flex items-center gap-4">
|
||||||
|
<div className="text-tertiary flex-shrink-0">{item.icon}</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-tertiary text-sm">{item.label}</div>
|
||||||
|
<div className="text-4xl font-bold leading-none my-0.5">{item.value}</div>
|
||||||
|
<div className="text-tertiary text-xs">Based on {formatCount(ratingData.count)} reviews</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RatingsCardRow;
|
||||||
82
src/components/Card/ScheduleCard/index.tsx
Normal file
82
src/components/Card/ScheduleCard/index.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
interface DaySchedule {
|
||||||
|
day: 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday';
|
||||||
|
open: string; // e.g. "7.00 AM"
|
||||||
|
close: string; // e.g. "21.00 PM"
|
||||||
|
closed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduleCardProps {
|
||||||
|
schedules: DaySchedule[];
|
||||||
|
onSuggestEdit?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DAYS: DaySchedule['day'][] = [
|
||||||
|
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getCurrentDay(): DaySchedule['day'] {
|
||||||
|
return DAYS[new Date().getDay()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpenNow(schedules: DaySchedule[]): boolean {
|
||||||
|
const today = getCurrentDay();
|
||||||
|
const todaySchedule = schedules.find((s) => s.day === today);
|
||||||
|
if (!todaySchedule || todaySchedule.closed) return false;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const [openHour, openMin] = todaySchedule.open.replace(/[^0-9.]/g, '').split('.').map(Number);
|
||||||
|
const [closeHour, closeMin] = todaySchedule.close.replace(/[^0-9.]/g, '').split('.').map(Number);
|
||||||
|
const openIsPm = todaySchedule.open.toUpperCase().includes('PM');
|
||||||
|
const closeIsPm = todaySchedule.close.toUpperCase().includes('PM');
|
||||||
|
|
||||||
|
const openTotal = (openIsPm && openHour !== 12 ? openHour + 12 : openHour) * 60 + (openMin || 0);
|
||||||
|
const closeTotal = (closeIsPm && closeHour !== 12 ? closeHour + 12 : closeHour) * 60 + (closeMin || 0);
|
||||||
|
const nowTotal = now.getHours() * 60 + now.getMinutes();
|
||||||
|
|
||||||
|
return nowTotal >= openTotal && nowTotal < closeTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScheduleCard({ schedules, onSuggestEdit }: ScheduleCardProps) {
|
||||||
|
const today = getCurrentDay();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-black/[0.27] rounded-xl mt-2 px-4 py-4 w-full">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold">Hours</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onSuggestEdit}
|
||||||
|
className="text-sm underline underline-offset-2 hover:text-white transition-colors text-tertiary mt-1"
|
||||||
|
>
|
||||||
|
Suggest an edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <hr className="border-[#38444d] mb-4 mt-2" /> */}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{DAYS.map((day) => {
|
||||||
|
const schedule = schedules.find((s) => s.day === day);
|
||||||
|
const isToday = day === today;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={day}
|
||||||
|
className={`flex items-center justify-between text-sm ${isToday ? 'font-bold text-white' : 'text-tertiary'}`}
|
||||||
|
>
|
||||||
|
<span>{day}</span>
|
||||||
|
<span>
|
||||||
|
{!schedule || schedule.closed
|
||||||
|
? 'Closed'
|
||||||
|
: `${schedule.open} - ${schedule.close}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScheduleCard;
|
||||||
@ -6,11 +6,13 @@ import { EmojiMatcher, PathConfig } from 'interweave-emoji';
|
|||||||
class SevenTVMatcher extends Matcher {
|
class SevenTVMatcher extends Matcher {
|
||||||
private emotes: Record<string, string> = {
|
private emotes: Record<string, string> = {
|
||||||
'booba': '01F6N31ETR0004P7N4A9PKS5X9',
|
'booba': '01F6N31ETR0004P7N4A9PKS5X9',
|
||||||
'pepeJAM': '5f1f0ea5cf6d2144653d7501',
|
'pepeJAM': '01EZY967K0000CYST6006V20T8',
|
||||||
'OMEGALUL': '5f4b3bc28fb088567e5cbb3b',
|
'OMEGALUL': '01F00Z3A9G0007E4VV006YKSK9',
|
||||||
'monkaS': '01F78CHJ2G0005TDSTZFBDGMK4',
|
'monkaS': '01F78CHJ2G0005TDSTZFBDGMK4',
|
||||||
'Sadge': '5f1f0f61b5e9d35e9a2f8a0e',
|
'Sadge': '01EZPG1FN80001SNAW00ADK2DY',
|
||||||
'PogU': '5f1f0c1235c7c40e6a3f9c1b',
|
'PogU': '01F6M3N17G000B5V5G2M2RYJN7',
|
||||||
|
'skateParkGe': '01GMKPJQJR0008GS0R6JQ1670A',
|
||||||
|
'crashout': '01KM204X52XJ5GAA1TAP6AZWGC'
|
||||||
};
|
};
|
||||||
|
|
||||||
replaceWith(match: string): Node {
|
replaceWith(match: string): Node {
|
||||||
@ -20,12 +22,12 @@ class SevenTVMatcher extends Matcher {
|
|||||||
if (emoteId) {
|
if (emoteId) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={`https://cdn.7tv.app/emote/${emoteId}/3x.avif`}
|
src={`https://cdn.7tv.app/emote/${emoteId}/4x.avif`}
|
||||||
alt={emoteName}
|
alt={emoteName}
|
||||||
title={emoteName}
|
title={emoteName}
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
height: '28px',
|
height: '32px',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
margin: '0 2px'
|
margin: '0 2px'
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user