feat: add collapsible component and update nearby search map view with new props and improved functionality
This commit is contained in:
parent
3e74a26274
commit
1c09e86fb7
@ -581,6 +581,8 @@ When asked a "What is" question, maintain the same format as the question and an
|
|||||||
|
|
||||||
const details = await detailsResponse.json();
|
const details = await detailsResponse.json();
|
||||||
|
|
||||||
|
console.log(`Place details for "${place.name}":`, details);
|
||||||
|
|
||||||
// Fetch place photos
|
// Fetch place photos
|
||||||
let photos = [];
|
let photos = [];
|
||||||
try {
|
try {
|
||||||
@ -611,24 +613,72 @@ When asked a "What is" question, maintain the same format as the question and an
|
|||||||
console.log(`Photo fetch failed for "${place.name}":`, error);
|
console.log(`Photo fetch failed for "${place.name}":`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process hours and status
|
|
||||||
const now = new Date();
|
|
||||||
const currentDay = now.getDay();
|
// Get timezone for the location
|
||||||
const currentTime = now.getHours() * 100 + now.getMinutes();
|
const tzResponse = await fetch(
|
||||||
|
`https://maps.googleapis.com/maps/api/timezone/json?location=${details.latitude},${details.longitude}×tamp=${Math.floor(Date.now() / 1000)}&key=${process.env.GOOGLE_MAPS_API_KEY}`
|
||||||
|
);
|
||||||
|
const tzData = await tzResponse.json();
|
||||||
|
const timezone = tzData.timeZoneId || 'UTC';
|
||||||
|
|
||||||
|
// Process hours and status with timezone
|
||||||
|
const localTime = new Date(new Date().toLocaleString('en-US', { timeZone: timezone }));
|
||||||
|
const currentDay = localTime.getDay();
|
||||||
|
const currentHour = localTime.getHours();
|
||||||
|
const currentMinute = localTime.getMinutes();
|
||||||
|
const currentTime = currentHour * 100 + currentMinute;
|
||||||
|
|
||||||
let is_closed = true;
|
let is_closed = true;
|
||||||
let next_open_close = '';
|
let next_open_close = null;
|
||||||
|
let next_day = currentDay;
|
||||||
|
|
||||||
if (details.hours?.periods) {
|
if (details.hours?.periods) {
|
||||||
const todayPeriod = details.hours.periods.find((period: any) =>
|
// Sort periods by day and time for proper handling of overnight hours
|
||||||
period.open?.day === currentDay
|
const sortedPeriods = [...details.hours.periods].sort((a, b) => {
|
||||||
);
|
if (a.open.day !== b.open.day) return a.open.day - b.open.day;
|
||||||
|
return parseInt(a.open.time) - parseInt(b.open.time);
|
||||||
|
});
|
||||||
|
|
||||||
if (todayPeriod) {
|
// Find current or next opening period
|
||||||
const openTime = parseInt(todayPeriod.open.time);
|
for (let i = 0; i < sortedPeriods.length; i++) {
|
||||||
const closeTime = todayPeriod.close ? parseInt(todayPeriod.close.time) : 2359;
|
const period = sortedPeriods[i];
|
||||||
is_closed = currentTime < openTime || currentTime > closeTime;
|
const openTime = parseInt(period.open.time);
|
||||||
next_open_close = is_closed ? todayPeriod.open.time : todayPeriod.close?.time;
|
const closeTime = period.close ? parseInt(period.close.time) : 2359;
|
||||||
|
const periodDay = period.open.day;
|
||||||
|
|
||||||
|
// Handle overnight hours
|
||||||
|
if (closeTime < openTime) {
|
||||||
|
// Place is open from previous day
|
||||||
|
if (currentDay === periodDay && currentTime < closeTime) {
|
||||||
|
is_closed = false;
|
||||||
|
next_open_close = period.close.time;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Place is open today and extends to tomorrow
|
||||||
|
if (currentDay === periodDay && currentTime >= openTime) {
|
||||||
|
is_closed = false;
|
||||||
|
next_open_close = period.close.time;
|
||||||
|
next_day = (periodDay + 1) % 7;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal hours within same day
|
||||||
|
if (currentDay === periodDay && currentTime >= openTime && currentTime < closeTime) {
|
||||||
|
is_closed = false;
|
||||||
|
next_open_close = period.close.time;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find next opening time if currently closed
|
||||||
|
if (is_closed) {
|
||||||
|
if ((periodDay > currentDay) || (periodDay === currentDay && openTime > currentTime)) {
|
||||||
|
next_open_close = period.open.time;
|
||||||
|
next_day = periodDay;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -639,6 +689,7 @@ When asked a "What is" question, maintain the same format as the question and an
|
|||||||
lat: parseFloat(details.latitude || place.latitude || finalLat),
|
lat: parseFloat(details.latitude || place.latitude || finalLat),
|
||||||
lng: parseFloat(details.longitude || place.longitude || finalLng)
|
lng: parseFloat(details.longitude || place.longitude || finalLng)
|
||||||
},
|
},
|
||||||
|
timezone,
|
||||||
place_id: place.location_id,
|
place_id: place.location_id,
|
||||||
vicinity: place.address_obj?.address_string || '',
|
vicinity: place.address_obj?.address_string || '',
|
||||||
distance: parseFloat(place.distance || '0'),
|
distance: parseFloat(place.distance || '0'),
|
||||||
@ -654,6 +705,8 @@ When asked a "What is" question, maintain the same format as the question and an
|
|||||||
is_closed,
|
is_closed,
|
||||||
hours: details.hours?.weekday_text || [],
|
hours: details.hours?.weekday_text || [],
|
||||||
next_open_close,
|
next_open_close,
|
||||||
|
next_day,
|
||||||
|
periods: details.hours?.periods || [],
|
||||||
photos,
|
photos,
|
||||||
source: details.source?.name || 'TripAdvisor'
|
source: details.source?.name || 'TripAdvisor'
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,8 +35,7 @@ interface Place {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
hours?: string[];
|
hours?: string[];
|
||||||
distance?: string;
|
timezone?: string;
|
||||||
bearing?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
|
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || '';
|
||||||
@ -45,8 +44,9 @@ interface InteractiveMapProps {
|
|||||||
center: Location;
|
center: Location;
|
||||||
places: Place[];
|
places: Place[];
|
||||||
selectedPlace: Place | null;
|
selectedPlace: Place | null;
|
||||||
onPlaceSelect: (place: Place) => void;
|
onPlaceSelect: (place: Place | null) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
viewMode?: 'map' | 'list';
|
||||||
}
|
}
|
||||||
|
|
||||||
const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
||||||
@ -54,7 +54,8 @@ const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
|||||||
places,
|
places,
|
||||||
selectedPlace,
|
selectedPlace,
|
||||||
onPlaceSelect,
|
onPlaceSelect,
|
||||||
className
|
className,
|
||||||
|
viewMode = 'map'
|
||||||
}) => {
|
}) => {
|
||||||
const mapContainerRef = useRef<HTMLDivElement>(null);
|
const mapContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const mapRef = useRef<mapboxgl.Map | null>(null);
|
const mapRef = useRef<mapboxgl.Map | null>(null);
|
||||||
@ -71,9 +72,9 @@ const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
|||||||
|
|
||||||
mapRef.current = new mapboxgl.Map({
|
mapRef.current = new mapboxgl.Map({
|
||||||
container: mapContainerRef.current,
|
container: mapContainerRef.current,
|
||||||
style: 'mapbox://styles/mapbox/standard',
|
style: 'mapbox://styles/mapbox/light-v11',
|
||||||
center: [center.lng, center.lat],
|
center: [center.lng, center.lat],
|
||||||
zoom: 13,
|
zoom: 14,
|
||||||
attributionControl: false,
|
attributionControl: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,14 +82,14 @@ const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
|||||||
|
|
||||||
// Add minimal controls
|
// Add minimal controls
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new mapboxgl.NavigationControl({ showCompass: false }),
|
new mapboxgl.NavigationControl({ showCompass: false, showZoom: true }),
|
||||||
'bottom-right',
|
'bottom-right'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Compact attribution
|
// Compact attribution
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new mapboxgl.AttributionControl({ compact: true }),
|
new mapboxgl.AttributionControl({ compact: true }),
|
||||||
'bottom-right'
|
'bottom-left'
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -105,14 +106,14 @@ const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
|||||||
Object.values(markersRef.current).forEach(marker => marker.remove());
|
Object.values(markersRef.current).forEach(marker => marker.remove());
|
||||||
markersRef.current = {};
|
markersRef.current = {};
|
||||||
|
|
||||||
// Add new markers
|
// Create markers with click handlers
|
||||||
places.forEach((place, index) => {
|
places.forEach((place, index) => {
|
||||||
const isSelected = selectedPlace?.name === place.name;
|
const isSelected = selectedPlace?.place_id === place.place_id;
|
||||||
|
|
||||||
// Create marker element
|
// Create marker element
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = cn(
|
el.className = cn(
|
||||||
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-300 cursor-pointer',
|
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-300 cursor-pointer shadow-md',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-black text-white scale-110'
|
? 'bg-black text-white scale-110'
|
||||||
: 'bg-white text-black hover:scale-105'
|
: 'bg-white text-black hover:scale-105'
|
||||||
@ -135,7 +136,7 @@ const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store marker reference
|
// Store marker reference
|
||||||
markersRef.current[place.name] = marker;
|
markersRef.current[place.place_id] = marker;
|
||||||
});
|
});
|
||||||
}, [places, selectedPlace, handleMarkerClick]);
|
}, [places, selectedPlace, handleMarkerClick]);
|
||||||
|
|
||||||
@ -153,7 +154,7 @@ const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
|||||||
|
|
||||||
// If click wasn't on a marker, deselect
|
// If click wasn't on a marker, deselect
|
||||||
if (!clickedMarker) {
|
if (!clickedMarker) {
|
||||||
onPlaceSelect(null as any); // Type cast to satisfy TS
|
onPlaceSelect(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -164,17 +165,65 @@ const InteractiveMap: React.FC<InteractiveMapProps> = ({
|
|||||||
};
|
};
|
||||||
}, [onPlaceSelect]);
|
}, [onPlaceSelect]);
|
||||||
|
|
||||||
// Fly to selected place
|
// Fly to selected place with proper padding for list view
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapRef.current || !selectedPlace) return;
|
if (!mapRef.current || !selectedPlace) return;
|
||||||
|
|
||||||
mapRef.current.flyTo({
|
const map = mapRef.current;
|
||||||
center: [selectedPlace.location.lng, selectedPlace.location.lat],
|
const { clientWidth, clientHeight } = document.documentElement;
|
||||||
zoom: 15,
|
|
||||||
|
// Calculate the actual width of list view (60% of viewport height in list mode)
|
||||||
|
const listHeight = viewMode === 'list' ? clientHeight * 0.6 : 0;
|
||||||
|
|
||||||
|
// Set padding based on view mode
|
||||||
|
const padding = {
|
||||||
|
top: viewMode === 'list' ? listHeight : 50,
|
||||||
|
bottom: 50,
|
||||||
|
left: 50,
|
||||||
|
right: 50
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get coordinates of the target location
|
||||||
|
const coordinates: [number, number] = [selectedPlace.location.lng, selectedPlace.location.lat];
|
||||||
|
|
||||||
|
// Calculate the optimal zoom level
|
||||||
|
const currentZoom = map.getZoom();
|
||||||
|
const targetZoom = currentZoom < 15 ? 15 : currentZoom;
|
||||||
|
|
||||||
|
// Fly to location with padding
|
||||||
|
map.flyTo({
|
||||||
|
center: coordinates,
|
||||||
|
zoom: targetZoom,
|
||||||
|
padding: padding,
|
||||||
duration: 1500,
|
duration: 1500,
|
||||||
essential: true,
|
essential: true
|
||||||
});
|
});
|
||||||
}, [selectedPlace]);
|
|
||||||
|
// Ensure padding is maintained after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
mapRef.current.setPadding(padding);
|
||||||
|
}
|
||||||
|
}, 1600);
|
||||||
|
|
||||||
|
}, [selectedPlace, viewMode]);
|
||||||
|
|
||||||
|
// Update map padding when view mode changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapRef.current) return;
|
||||||
|
|
||||||
|
const { clientHeight } = document.documentElement;
|
||||||
|
const listHeight = viewMode === 'list' ? clientHeight * 0.6 : 0;
|
||||||
|
|
||||||
|
const padding = {
|
||||||
|
top: viewMode === 'list' ? listHeight : 50,
|
||||||
|
bottom: 50,
|
||||||
|
left: 50,
|
||||||
|
right: 50
|
||||||
|
};
|
||||||
|
|
||||||
|
mapRef.current.setPadding(padding);
|
||||||
|
}, [viewMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full h-full relative z-0", className)}>
|
<div className={cn("w-full h-full relative z-0", className)}>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import PlaceCard from './place-card';
|
|||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface Location {
|
interface Location {
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
@ -38,8 +39,9 @@ interface Place {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
hours?: string[];
|
hours?: string[];
|
||||||
distance?: string;
|
distance?: number;
|
||||||
bearing?: string;
|
bearing?: string;
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic import for the map component
|
// Dynamic import for the map component
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import PlaceholderImage from './placeholder-image';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
MapPin, Star, ExternalLink, Navigation, Globe, Phone, ChevronDown, ChevronUp,
|
||||||
|
Clock
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface Location {
|
interface Location {
|
||||||
lat: number;
|
lat: number;
|
||||||
@ -37,8 +42,9 @@ interface Place {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
hours?: string[];
|
hours?: string[];
|
||||||
distance?: string;
|
distance?: number;
|
||||||
bearing?: string;
|
bearing?: string;
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaceCardProps {
|
interface PlaceCardProps {
|
||||||
@ -48,133 +54,191 @@ interface PlaceCardProps {
|
|||||||
variant?: 'overlay' | 'list';
|
variant?: 'overlay' | 'list';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const HoursSection: React.FC<{ hours: string[]; timezone?: string }> = ({ hours, timezone }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const now = timezone ?
|
||||||
|
DateTime.now().setZone(timezone) :
|
||||||
|
DateTime.now();
|
||||||
|
const currentDay = now.weekdayLong;
|
||||||
|
|
||||||
|
if (!hours?.length) return null;
|
||||||
|
|
||||||
|
// Find today's hours
|
||||||
|
const todayHours = hours.find(h => h.startsWith(currentDay!))?.split(': ')[1] || 'Closed';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 border-t dark:border-neutral-800">
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"mt-4 flex items-center gap-2 cursor-pointer transition-colors",
|
||||||
|
"text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Clock className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span>Today: <span className="font-medium text-neutral-900 dark:text-neutral-100">{todayHours}</span></span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
className="ml-auto p-0 h-8 w-8 hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{isOpen ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"grid transition-all duration-200 overflow-hidden",
|
||||||
|
isOpen ? "grid-rows-[1fr] mt-2" : "grid-rows-[0fr]"
|
||||||
|
)}>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div className="rounded-md border dark:border-neutral-800 divide-y divide-neutral-100 dark:divide-neutral-800 bg-neutral-50 dark:bg-neutral-900">
|
||||||
|
{hours.map((timeSlot, idx) => {
|
||||||
|
const [day, hours] = timeSlot.split(': ');
|
||||||
|
const isToday = day === currentDay;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between py-2 px-3 text-sm rounded-md",
|
||||||
|
isToday && "bg-white dark:bg-neutral-800"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn(
|
||||||
|
"font-medium",
|
||||||
|
isToday ? "text-primary" : "text-neutral-600 dark:text-neutral-400"
|
||||||
|
)}>
|
||||||
|
{day}
|
||||||
|
</span>
|
||||||
|
<span className={cn(
|
||||||
|
isToday ? "font-medium" : "text-neutral-600 dark:text-neutral-400"
|
||||||
|
)}>
|
||||||
|
{hours}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const PlaceCard: React.FC<PlaceCardProps> = ({
|
const PlaceCard: React.FC<PlaceCardProps> = ({
|
||||||
place,
|
place,
|
||||||
onClick,
|
onClick,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
variant = 'list'
|
variant = 'list'
|
||||||
}) => {
|
}) => {
|
||||||
|
const [showHours, setShowHours] = useState(false);
|
||||||
const isOverlay = variant === 'overlay';
|
const isOverlay = variant === 'overlay';
|
||||||
|
|
||||||
// Validation helpers from before...
|
const formatTime = (timeStr: string | undefined, timezone: string | undefined): string => {
|
||||||
const isValidString = (str: any): boolean => {
|
if (!timeStr || !timezone) return '';
|
||||||
return str !== undefined &&
|
const hours = Math.floor(parseInt(timeStr) / 100);
|
||||||
str !== null &&
|
const minutes = parseInt(timeStr) % 100;
|
||||||
String(str).trim() !== '' &&
|
return DateTime.now()
|
||||||
String(str).toLowerCase() !== 'undefined' &&
|
.setZone(timezone)
|
||||||
String(str).toLowerCase() !== 'null';
|
.set({ hour: hours, minute: minutes })
|
||||||
|
.toFormat('h:mm a');
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValidNumber = (num: any): boolean => {
|
const getStatusDisplay = (): { text: string; color: string } | null => {
|
||||||
if (num === undefined || num === null) return false;
|
if (!place.timezone || place.is_closed === undefined || !place.next_open_close) {
|
||||||
const parsed = Number(num);
|
return null;
|
||||||
return !isNaN(parsed) && isFinite(parsed) && parsed !== 0;
|
}
|
||||||
|
|
||||||
|
const timeStr = formatTime(place.next_open_close, place.timezone);
|
||||||
|
if (place.is_closed) {
|
||||||
|
return {
|
||||||
|
text: `Closed · Opens ${timeStr}`,
|
||||||
|
color: 'red-600 dark:text-red-400'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: `Open · Closes ${timeStr}`,
|
||||||
|
color: 'green-600 dark:text-green-400'
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatRating = (rating: any): string => {
|
const statusDisplay = getStatusDisplay();
|
||||||
if (!isValidNumber(rating)) return '';
|
|
||||||
const parsed = Number(rating);
|
|
||||||
return parsed.toFixed(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const cardContent = (
|
||||||
<div
|
<>
|
||||||
onClick={onClick}
|
<div className="flex gap-3">
|
||||||
className={cn(
|
{/* Image with Price Badge */}
|
||||||
"transition-all duration-200 cursor-pointer rounded-lg",
|
{place.photos?.[0]?.medium && (
|
||||||
variant === 'overlay'
|
<div className="relative w-20 h-20 rounded-md overflow-hidden flex-shrink-0">
|
||||||
? 'bg-white/90 dark:bg-black/90 backdrop-blur-sm'
|
|
||||||
: 'bg-white dark:bg-neutral-900 hover:bg-neutral-50 dark:hover:bg-neutral-800',
|
|
||||||
isSelected && variant !== 'overlay' && 'ring-2 ring-primary dark:ring-primary',
|
|
||||||
'border border-neutral-200 dark:border-neutral-800'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 p-4">
|
|
||||||
{/* Image Container */}
|
|
||||||
<div className="w-full sm:w-24 h-40 sm:h-24 rounded-lg overflow-hidden flex-shrink-0">
|
|
||||||
{place.photos?.[0]?.medium ? (
|
|
||||||
<img
|
<img
|
||||||
src={place.photos[0].medium}
|
src={place.photos[0].medium}
|
||||||
alt={place.name}
|
alt={place.name}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
{place.price_level && (
|
||||||
<PlaceholderImage />
|
<div className="absolute top-0 left-0 bg-black/80 text-white px-2 py-0.5 text-xs font-medium">
|
||||||
|
{place.price_level}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{/* Title Section */}
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-2 mb-2">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="text-xl font-medium text-neutral-900 dark:text-white truncate">
|
<h3 className="font-semibold truncate pr-6">
|
||||||
{place.name}
|
{place.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isValidNumber(place.rating) && (
|
{/* Rating & Reviews */}
|
||||||
<div className="flex items-center gap-2 text-sm">
|
{place.rating && (
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<div className="flex items-center gap-1 mt-1">
|
||||||
{formatRating(place.rating)}
|
<Star className="w-4 h-4 text-yellow-500 fill-yellow-500" />
|
||||||
</span>
|
<span className="font-medium">{place.rating.toFixed(1)}</span>
|
||||||
{isValidNumber(place.reviews_count) && (
|
{place.reviews_count && (
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">
|
<span className="text-neutral-500">({place.reviews_count})</span>
|
||||||
({place.reviews_count} reviews)
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status & Info */}
|
{/* Status */}
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 mb-2">
|
{statusDisplay && (
|
||||||
{place.is_closed !== undefined && (
|
<div className={`text-sm text-${statusDisplay.color} mt-1`}>
|
||||||
<span className={cn(
|
{statusDisplay.text}
|
||||||
"text-sm font-medium",
|
</div>
|
||||||
place.is_closed
|
|
||||||
? "text-red-600 dark:text-red-400"
|
|
||||||
: "text-green-600 dark:text-green-400"
|
|
||||||
)}>
|
|
||||||
{place.is_closed ? "Closed" : "Open now"}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{isValidString(place.next_open_close) && (
|
|
||||||
<>
|
{/* Address */}
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">·</span>
|
{place.vicinity && (
|
||||||
<span className="text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
||||||
until {place.next_open_close}
|
<MapPin className="w-4 h-4 mr-1 flex-shrink-0" />
|
||||||
</span>
|
<span className="truncate">{place.vicinity}</span>
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
{isValidString(place.type) && (
|
|
||||||
<>
|
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">·</span>
|
|
||||||
<span className="text-sm text-neutral-500 dark:text-neutral-400 capitalize">
|
|
||||||
{place.type}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{isValidString(place.price_level) && (
|
|
||||||
<>
|
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">·</span>
|
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">
|
|
||||||
{place.price_level}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Description */}
|
|
||||||
{isValidString(place.description) && (
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-2 mb-3">
|
|
||||||
{place.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="grid grid-cols-2 sm:flex gap-2">
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full sm:w-auto bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700 text-neutral-900 dark:text-white"
|
className="h-8"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
window.open(
|
window.open(
|
||||||
@ -183,52 +247,88 @@ const PlaceCard: React.FC<PlaceCardProps> = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Navigation className="w-4 h-4 mr-2" />
|
||||||
Directions
|
Directions
|
||||||
</Button>
|
</Button>
|
||||||
{isValidString(place.website) && (
|
|
||||||
|
{place.phone && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full sm:w-auto bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700 text-neutral-900 dark:text-white"
|
className="h-8"
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
window.open(place.website, '_blank');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Website
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isValidString(place.phone) && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
className="w-full sm:w-auto bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700 text-neutral-900 dark:text-white"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
window.open(`tel:${place.phone}`, '_blank');
|
window.open(`tel:${place.phone}`, '_blank');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Phone className="w-4 h-4 mr-2" />
|
||||||
Call
|
Call
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isValidString(place.place_id) && (
|
|
||||||
|
{place.website && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full sm:w-auto bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700 text-neutral-900 dark:text-white"
|
className="h-8"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(place.website, '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Globe className="w-4 h-4 mr-2" />
|
||||||
|
Website
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{place.place_id && !isOverlay && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
window.open(`https://www.tripadvisor.com/${place.place_id}`, '_blank');
|
window.open(`https://www.tripadvisor.com/${place.place_id}`, '_blank');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
TripAdvisor
|
<ExternalLink className="w-4 h-4 mr-2" />
|
||||||
|
More Info
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hours Section - Only show if has hours */}
|
||||||
|
{place.hours && place.hours.length > 0 && (
|
||||||
|
<HoursSection hours={place.hours} timezone={place.timezone} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isOverlay) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-white/95 dark:bg-black/95 backdrop-blur-sm p-4 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-800"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{cardContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"w-full transition-all duration-200 cursor-pointer p-4",
|
||||||
|
"hover:bg-neutral-50 dark:hover:bg-neutral-800",
|
||||||
|
isSelected && "ring-2 ring-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cardContent}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PlaceCard;
|
export default PlaceCard;
|
||||||
11
components/ui/collapsible.tsx
Normal file
11
components/ui/collapsible.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
|
const Collapsible = CollapsiblePrimitive.Root
|
||||||
|
|
||||||
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
|
||||||
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
@ -20,6 +20,7 @@
|
|||||||
"@foobar404/wave": "^2.0.5",
|
"@foobar404/wave": "^2.0.5",
|
||||||
"@mendable/firecrawl-js": "^1.4.3",
|
"@mendable/firecrawl-js": "^1.4.3",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
@ -56,6 +57,7 @@
|
|||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"lucide-react": "^0.424.0",
|
"lucide-react": "^0.424.0",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
"mapbox-gl": "^3.7.0",
|
"mapbox-gl": "^3.7.0",
|
||||||
"marked-react": "^2.0.0",
|
"marked-react": "^2.0.0",
|
||||||
"next": "^14.2.17",
|
"next": "^14.2.17",
|
||||||
@ -80,6 +82,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/google.maps": "^3.55.12",
|
"@types/google.maps": "^3.55.12",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
|||||||
@ -13,7 +13,7 @@ dependencies:
|
|||||||
version: 0.0.51(zod@3.23.8)
|
version: 0.0.51(zod@3.23.8)
|
||||||
'@ai-sdk/cohere':
|
'@ai-sdk/cohere':
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 0.0.28(zod@3.23.8)
|
version: 1.0.0(zod@3.23.8)
|
||||||
'@ai-sdk/google':
|
'@ai-sdk/google':
|
||||||
specifier: ^0.0.55
|
specifier: ^0.0.55
|
||||||
version: 0.0.55(zod@3.23.8)
|
version: 0.0.55(zod@3.23.8)
|
||||||
@ -38,6 +38,9 @@ dependencies:
|
|||||||
'@radix-ui/react-accordion':
|
'@radix-ui/react-accordion':
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
|
version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
|
||||||
|
'@radix-ui/react-collapsible':
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
|
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1)
|
||||||
@ -146,6 +149,9 @@ dependencies:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.424.0
|
specifier: ^0.424.0
|
||||||
version: 0.424.0(react@18.3.1)
|
version: 0.424.0(react@18.3.1)
|
||||||
|
luxon:
|
||||||
|
specifier: ^3.5.0
|
||||||
|
version: 3.5.0
|
||||||
mapbox-gl:
|
mapbox-gl:
|
||||||
specifier: ^3.7.0
|
specifier: ^3.7.0
|
||||||
version: 3.7.0
|
version: 3.7.0
|
||||||
@ -214,6 +220,9 @@ devDependencies:
|
|||||||
'@types/google.maps':
|
'@types/google.maps':
|
||||||
specifier: ^3.55.12
|
specifier: ^3.55.12
|
||||||
version: 3.58.1
|
version: 3.58.1
|
||||||
|
'@types/luxon':
|
||||||
|
specifier: ^3.4.2
|
||||||
|
version: 3.4.2
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20
|
specifier: ^20
|
||||||
version: 20.16.11
|
version: 20.16.11
|
||||||
@ -270,14 +279,14 @@ packages:
|
|||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@ai-sdk/cohere@0.0.28(zod@3.23.8):
|
/@ai-sdk/cohere@1.0.0(zod@3.23.8):
|
||||||
resolution: {integrity: sha512-aWavC7PuC5AcDcwTBcvx2ZemudtjPaZvLDmcVjiIhzvvW2+zoHoWmERD8CtSWIMlQp/vjdTcwNhSXTOqfxm27A==}
|
resolution: {integrity: sha512-iN2Ww2VeRnprQBJ7dCp65DtdzCY/53+CA3UmM7Rhn8IZCTqRHkVoZebzv3ZOTb9pikO4CUWqV9yh1oUhQDgyow==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 0.0.26
|
'@ai-sdk/provider': 1.0.0
|
||||||
'@ai-sdk/provider-utils': 1.0.22(zod@3.23.8)
|
'@ai-sdk/provider-utils': 2.0.0(zod@3.23.8)
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -416,6 +425,22 @@ packages:
|
|||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@ai-sdk/provider-utils@2.0.0(zod@3.23.8):
|
||||||
|
resolution: {integrity: sha512-uITgVJByhtzuQU2ZW+2CidWRmQqTUTp6KADevy+4aRnmILZxY2LCt+UZ/ZtjJqq0MffwkuQPPY21ExmFAQ6kKA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider': 1.0.0
|
||||||
|
eventsource-parser: 3.0.0
|
||||||
|
nanoid: 5.0.8
|
||||||
|
secure-json-parse: 2.7.0
|
||||||
|
zod: 3.23.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@ai-sdk/provider@0.0.22:
|
/@ai-sdk/provider@0.0.22:
|
||||||
resolution: {integrity: sha512-smZ1/2jL/JSKnbhC6ama/PxI2D/psj+YAe0c0qpd5ComQCNFltg72VFf0rpUSFMmFuj1pCCNoBOCrvyl8HTZHQ==}
|
resolution: {integrity: sha512-smZ1/2jL/JSKnbhC6ama/PxI2D/psj+YAe0c0qpd5ComQCNFltg72VFf0rpUSFMmFuj1pCCNoBOCrvyl8HTZHQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -444,6 +469,13 @@ packages:
|
|||||||
json-schema: 0.4.0
|
json-schema: 0.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@ai-sdk/provider@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Sj29AzooJ7SYvhPd+AAWt/E7j63E9+AzRnoMHUaJPRYzOd/WDrVNxxv85prF9gDcQ7XPVlSk9j6oAZV9/DXYpA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
json-schema: 0.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@ai-sdk/react@0.0.70(react@18.3.1)(zod@3.23.8):
|
/@ai-sdk/react@0.0.70(react@18.3.1)(zod@3.23.8):
|
||||||
resolution: {integrity: sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==}
|
resolution: {integrity: sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -1843,6 +1875,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/luxon@3.4.2:
|
||||||
|
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/mapbox-gl@3.4.0:
|
/@types/mapbox-gl@3.4.0:
|
||||||
resolution: {integrity: sha512-tbn++Mm94H1kE7W6FF0oVC9rMXHVzDDNUbS7KfBMRF8NV/8csFi+67ytKcZJ4LsrpsJ+8MC6Os6ZinEDCsrunw==}
|
resolution: {integrity: sha512-tbn++Mm94H1kE7W6FF0oVC9rMXHVzDDNUbS7KfBMRF8NV/8csFi+67ytKcZJ4LsrpsJ+8MC6Os6ZinEDCsrunw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3528,6 +3564,11 @@ packages:
|
|||||||
engines: {node: '>=14.18'}
|
engines: {node: '>=14.18'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/eventsource-parser@3.0.0:
|
||||||
|
resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/extend@3.0.2:
|
/extend@3.0.2:
|
||||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -4613,6 +4654,11 @@ packages:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/luxon@3.5.0:
|
||||||
|
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/magic-string@0.30.11:
|
/magic-string@0.30.11:
|
||||||
resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
|
resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5205,6 +5251,12 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/nanoid@5.0.8:
|
||||||
|
resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/natural-compare@1.4.0:
|
/natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user