From 0f2628928b907aee15b284135c0490436a2fee26 Mon Sep 17 00:00:00 2001 From: goro Date: Sun, 12 Apr 2026 10:22:47 +0300 Subject: [PATCH] added bunch of cards and adjust new 7tv emote --- src/components/Card/FacilitiesCard/index.tsx | 155 ++++++++++++++++++ src/components/Card/RatingsCard/index.tsx | 163 ++++++++----------- src/components/Card/RatingsCardRow/index.tsx | 79 +++++++++ src/components/Card/ScheduleCard/index.tsx | 82 ++++++++++ src/components/CustomInterweave/index.tsx | 14 +- 5 files changed, 388 insertions(+), 105 deletions(-) create mode 100644 src/components/Card/FacilitiesCard/index.tsx create mode 100644 src/components/Card/RatingsCardRow/index.tsx create mode 100644 src/components/Card/ScheduleCard/index.tsx diff --git a/src/components/Card/FacilitiesCard/index.tsx b/src/components/Card/FacilitiesCard/index.tsx new file mode 100644 index 0000000..124b1a0 --- /dev/null +++ b/src/components/Card/FacilitiesCard/index.tsx @@ -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 ( +
+

{category.title}

+
    + {category.items.map((item, i) => ( +
  1. + {item.text} +
  2. + ))} +
+
+ ); +} + +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 ( + <> +
+

{title}

+
+
+ {left.map((cat, i) => )} +
+
+ {middle.map((cat, i) => )} +
+
+ {right.map((cat, i) => )} +
+
+ {seeMoreShow !== undefined && ( +
+ +
+ )} +
+ + {/* Dialog */} + + setDialogOpen(false)} className="relative z-50"> + + {/* Backdrop */} + +
+ + + {/* Panel container */} +
+ + + + {/* Close button — top right */} + + + {/* Header */} +
+ {title} + + {/* Pill tabs */} + + + {allCategories.map((cat, i) => ( + + {cat.title} + + ))} + + + {/* Tab panels */} + + {allCategories.map((cat, i) => ( + +
+ {cat.items.map((item, j) => ( +
+ {item.text} +
+ ))} +
+
+ ))} +
+
+
+ +
+
+
+ +
+
+ + ); +} + +export default FacilitiesCard; diff --git a/src/components/Card/RatingsCard/index.tsx b/src/components/Card/RatingsCard/index.tsx index 722e392..115dcad 100644 --- a/src/components/Card/RatingsCard/index.tsx +++ b/src/components/Card/RatingsCard/index.tsx @@ -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 { score: number; count: number; @@ -41,117 +46,77 @@ const RatingsCard = ({ }; return ( -
- {/* Critics Score Section */} -
-
-
CRITICS SCORE
-
- {calculateScore(criticData.score, criticData.count)} +
+
+ + {/* Critics column */} +
+
+
+
CRITICS SCORE
+ {criticData.count !== 0 && ( +
Based on {formatCount(criticData.count)} reviews
+ )} +
+
+ {calculateScore(criticData.score, criticData.count)} +
-
-
-
- {criticData.count !== 0 && ( -
- Based on {formatCount(criticData.count)} reviews + {criticDetails && ( +
+ {([ + { label: 'Taste', value: criticDetails.environment, icon: }, + { label: 'Cleanliness', value: criticDetails.cleanliness, icon: }, + { label: 'Service', value: criticDetails.price, icon: }, + { label: 'Facilities', value: criticDetails.facility, icon: }, + ] as const).map((item) => ( +
+
{item.icon}
+
+
{item.label}
+
{item.value}
+
Based on {formatCount(criticData.count)} reviews
+
+
+ ))}
)}
- {/* Critics Detail Cards */} - {criticDetails && ( -
-
-
ENVIRONMENT
-
-
{criticDetails.environment}
-
-
-
-
+ {/* Users column */} +
+
+
+
USERS SCORE
+ {userData.count !== 0 && ( +
Based on {formatCount(userData.count)} reviews
+ )}
- -
-
PRICE
-
-
{criticDetails.price}
-
-
-
-
-
- -
-
FACILITY
-
-
{criticDetails.facility}
-
-
-
-
+
+ {calculateScore(userData.score, userData.count)}
- )} -
- - {/* Users Score Section */} -
-
-
USERS SCORE
-
- {calculateScore(userData.score, userData.count)} -
-
-
-
- {userData.count !== 0 && ( -
- Based on {formatCount(userData.count)} reviews + {userDetails && ( +
+ {([ + { label: 'Taste', value: userDetails.environment, icon: }, + { label: 'Cleanliness', value: userDetails.cleanliness, icon: }, + { label: 'Service', value: userDetails.price, icon: }, + { label: 'Facilities', value: userDetails.facility, icon: }, + ] as const).map((item) => ( +
+
{item.icon}
+
+
{item.label}
+
{item.value}
+
Based on {formatCount(userData.count)} reviews
+
+
+ ))}
)}
- {/* Users Detail Cards */} - {userDetails && ( -
-
-
ENVIRONMENT
-
-
{userDetails.environment}
-
-
-
-
-
- -
-
PRICE
-
-
{userDetails.price}
-
-
-
-
-
- -
-
FACILITY
-
-
{userDetails.facility}
-
-
-
-
-
-
- )}
); diff --git a/src/components/Card/RatingsCardRow/index.tsx b/src/components/Card/RatingsCardRow/index.tsx new file mode 100644 index 0000000..4e480e6 --- /dev/null +++ b/src/components/Card/RatingsCardRow/index.tsx @@ -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 { + data: T; + title: string; + getRatingData: (data: T) => RatingData; + getDetails?: (data: T) => DetailRatings; +} + +const RatingsCardRow = ({ + data, + title, + getRatingData, + getDetails, +}: RatingsCardRowProps) => { + 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 ( +
+
+
+
{title}
+
+ {score} +
+
+ {ratingData.count !== 0 && ( +
Based on {formatCount(ratingData.count)} reviews
+ )} +
+ + {details && ( +
+ {([ + { label: 'Rasa', value: details.environment, icon: }, + { label: 'Kebersihan', value: details.cleanliness, icon: }, + { label: 'Pelayanan', value: details.price, icon: }, + { label: 'Fasilitas', value: details.facility, icon: }, + ] as const).map((item) => ( +
+
{item.icon}
+
+
{item.label}
+
{item.value}
+
Based on {formatCount(ratingData.count)} reviews
+
+
+ ))} +
+ )} +
+ ); +}; + +export default RatingsCardRow; diff --git a/src/components/Card/ScheduleCard/index.tsx b/src/components/Card/ScheduleCard/index.tsx new file mode 100644 index 0000000..d417fdf --- /dev/null +++ b/src/components/Card/ScheduleCard/index.tsx @@ -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 ( +
+
+
+

Hours

+
+ +
+ + {/*
*/} + +
+ {DAYS.map((day) => { + const schedule = schedules.find((s) => s.day === day); + const isToday = day === today; + + return ( +
+ {day} + + {!schedule || schedule.closed + ? 'Closed' + : `${schedule.open} - ${schedule.close}`} + +
+ ); + })} +
+
+ ); +} + +export default ScheduleCard; diff --git a/src/components/CustomInterweave/index.tsx b/src/components/CustomInterweave/index.tsx index fe7f18e..0d6dca8 100755 --- a/src/components/CustomInterweave/index.tsx +++ b/src/components/CustomInterweave/index.tsx @@ -6,11 +6,13 @@ import { EmojiMatcher, PathConfig } from 'interweave-emoji'; class SevenTVMatcher extends Matcher { private emotes: Record = { 'booba': '01F6N31ETR0004P7N4A9PKS5X9', - 'pepeJAM': '5f1f0ea5cf6d2144653d7501', - 'OMEGALUL': '5f4b3bc28fb088567e5cbb3b', + 'pepeJAM': '01EZY967K0000CYST6006V20T8', + 'OMEGALUL': '01F00Z3A9G0007E4VV006YKSK9', 'monkaS': '01F78CHJ2G0005TDSTZFBDGMK4', - 'Sadge': '5f1f0f61b5e9d35e9a2f8a0e', - 'PogU': '5f1f0c1235c7c40e6a3f9c1b', + 'Sadge': '01EZPG1FN80001SNAW00ADK2DY', + 'PogU': '01F6M3N17G000B5V5G2M2RYJN7', + 'skateParkGe': '01GMKPJQJR0008GS0R6JQ1670A', + 'crashout': '01KM204X52XJ5GAA1TAP6AZWGC' }; replaceWith(match: string): Node { @@ -20,12 +22,12 @@ class SevenTVMatcher extends Matcher { if (emoteId) { return (