| import { useState } from 'react'; |
| import passportMomoPage from '../assets/passport-momo-page.jpg'; |
| import { ImageBackedPage, withNavHotspots, type ImageHotspot } from './ImageBackedPage'; |
|
|
| type PassportMarker = {
|
| id: string;
|
| kind: 'visited' | 'dream';
|
| flag: string;
|
| country: string;
|
| city: string;
|
| left: string;
|
| top: string;
|
| };
|
|
|
| const passportMarkers: PassportMarker[] = [ |
| { id: 'usa', kind: 'visited', flag: 'US', country: 'USA', city: 'New York', left: '30.2%', top: '23.3%' }, |
| { id: 'france', kind: 'visited', flag: 'FR', country: 'France', city: 'Paris', left: '50.0%', top: '21.8%' }, |
| { id: 'egypt', kind: 'visited', flag: 'EG', country: 'Egypt', city: 'Cairo', left: '63.2%', top: '25.8%' }, |
| { id: 'brazil', kind: 'visited', flag: 'BR', country: 'Brazil', city: 'Rio de Janeiro', left: '35.6%', top: '35.3%' }, |
| { id: 'thailand', kind: 'visited', flag: 'TH', country: 'Thailand', city: 'Bangkok', left: '80.2%', top: '30.9%' }, |
| { id: 'japan', kind: 'visited', flag: 'JP', country: 'Japan', city: 'Kyoto', left: '82.6%', top: '21.4%' }, |
| { id: 'greenland', kind: 'dream', flag: 'GL', country: 'Greenland', city: 'Nuuk', left: '43.0%', top: '17.8%' }, |
| { id: 'morocco', kind: 'dream', flag: 'MA', country: 'Morocco', city: 'Marrakech', left: '45.8%', top: '27.0%' }, |
| { id: 'india', kind: 'dream', flag: 'IN', country: 'India', city: 'Jaipur', left: '70.2%', top: '21.7%' }, |
| { id: 'australia', kind: 'dream', flag: 'AU', country: 'Australia', city: 'Sydney', left: '82.8%', top: '36.8%' }, |
| ]; |
|
|
| export function MapTab({ |
| onNavigate, |
| onToast, |
| onOpenScene: _onOpenScene, |
| }: { |
| onNavigate: (tabId: string) => void; |
| onToast: (message: string) => void; |
| onOpenScene: (sceneId: string) => void; |
| }) { |
| const [activeMarkerId, setActiveMarkerId] = useState<string | null>(null); |
| const activeMarker = passportMarkers.find((marker) => marker.id === activeMarkerId); |
| const stampHotspots: ImageHotspot[] = [ |
| { label: 'Japan culture card', left: '4.8%', top: '53.0%', width: '16.0%', height: '15.5%', onClick: () => onNavigate('journal') }, |
| { label: 'France culture card', left: '22.2%', top: '53.0%', width: '13.0%', height: '15.5%', onClick: () => onToast('France passport progress opened.') }, |
| { label: 'Thailand culture card', left: '49.5%', top: '53.0%', width: '14.5%', height: '15.5%', onClick: () => onToast('Thailand passport progress opened.') }, |
| { label: 'Morocco culture card', left: '66.4%', top: '53.0%', width: '14.5%', height: '15.5%', onClick: () => onToast('Morocco passport progress opened.') }, |
| { label: 'Egypt culture card', left: '82.5%', top: '53.0%', width: '13.5%', height: '15.5%', onClick: () => onToast('Egypt passport progress opened.') }, |
| { label: 'Japan stamp', left: '6.0%', top: '75.6%', width: '12.0%', height: '6.8%', onClick: () => onNavigate('journal') }, |
| { label: 'France stamp', left: '22.5%', top: '75.6%', width: '12.0%', height: '6.8%', onClick: () => onNavigate('journal') }, |
| { label: 'Thailand stamp', left: '39.5%', top: '75.6%', width: '12.5%', height: '6.8%', onClick: () => onToast('Thailand stamp collection opened.') }, |
| { label: 'Morocco stamp', left: '56.5%', top: '75.6%', width: '12.5%', height: '6.8%', onClick: () => onToast('Morocco stamp collection opened.') }, |
| { label: 'Egypt stamp', left: '73.0%', top: '75.6%', width: '12.5%', height: '6.8%', onClick: () => onToast('Egypt stamp collection opened.') }, |
| { label: 'More stamps', left: '88.0%', top: '74.8%', width: '9.0%', height: '8.5%', onClick: () => onToast('More passport stamps are coming soon after the hackathon.') }, |
| { label: 'Cultural explorer progress', left: '5.0%', top: '84.3%', width: '90.0%', height: '7.0%', onClick: () => onToast('Cultural Explorer level 5: 2,340 / 3,000 XP.') }, |
| ]; |
|
|
| return ( |
| <ImageBackedPage |
| image={passportMomoPage} |
| alt="CultureLens passport page with Momo and a collectible globe" |
| aspectRatio="854 / 1842" |
| hotspots={withNavHotspots(stampHotspots, onNavigate)} |
| > |
| {passportMarkers.map((marker) => ( |
| <button |
| key={marker.id}
|
| type="button"
|
| aria-label={`${marker.country}, ${marker.city}`}
|
| className="absolute z-20 h-10 w-10 -translate-x-1/2 -translate-y-1/2 rounded-full bg-transparent transition-all hover:bg-[#FFF4D8]/20 hover:ring-2 hover:ring-[#FFF4D8] focus:outline-none focus-visible:bg-[#FFF4D8]/25 focus-visible:ring-2 focus-visible:ring-[#FFF4D8]" |
| style={{ left: marker.left, top: marker.top }} |
| onClick={() => { |
| setActiveMarkerId(activeMarkerId === marker.id ? null : marker.id); |
| onToast(`${marker.country} passport marker opened.`); |
| }} |
| /> |
| ))} |
| |
| {activeMarker && <MarkerFlag marker={activeMarker} />} |
| </ImageBackedPage> |
| ); |
| } |
|
|
| function MarkerFlag({ marker }: { marker: PassportMarker }) { |
| const isRightSide = parseFloat(marker.left) > 72;
|
| const isLow = parseFloat(marker.top) > 55;
|
|
|
| return (
|
| <div
|
| className={`absolute z-30 w-[150px] bg-[#FFF8E7] pixel-border-sm pixel-shadow-sm px-3 py-2 ${isRightSide ? '-translate-x-full -ml-4' : 'ml-4'} ${isLow ? '-translate-y-full -mt-3' : '-translate-y-1/2'}`}
|
| style={{ left: marker.left, top: marker.top }}
|
| >
|
| <div className="font-pixel text-[8px] leading-tight text-wood">
|
| {marker.flag} {marker.country}
|
| </div>
|
| <div className="mt-1 font-gothic text-xs font-black text-border">{marker.city}</div>
|
| <div className={`mt-1 font-gothic text-[11px] font-black ${marker.kind === 'visited' ? 'text-[#FF8A65]' : 'text-[#4A90E2]'}`}>
|
| {marker.kind === 'visited' ? 'Unlocked' : 'Dream Trip'}
|
| </div>
|
| </div>
|
| );
|
| }
|
|
|