page.tsx (6215B)
1 "use client"; 2 import { useEffect, useState, useMemo } from "react"; 3 import { 4 APIProvider, 5 Map, 6 Marker, 7 useApiIsLoaded, 8 } from "@vis.gl/react-google-maps"; 9 import places, { Place } from "./data"; 10 //import { useSearchParams } from "next/navigation"; 11 12 /* 13 const findPlaceById = (id: number): Place | undefined => { 14 return places.find((place) => place.id === id); 15 }; 16 */ 17 18 type Coords = { 19 lat: number; 20 lng: number; 21 } | null; 22 23 function markerLabel(text: string) { 24 return { 25 text: text, 26 color: "#000", 27 fontSize: "0.8rem", 28 fontWeight: "500", 29 fontFamily: "Noto Sans JP, sans-serif", 30 }; 31 } 32 33 export default function Home() { 34 const [selectedPlace, setSelectedPlace] = useState<Place | null>(null); 35 const [userLocation, setUserLocation] = useState<Coords>(null); 36 const [locationLoading, setLocationLoading] = useState(true); 37 const [currentZoom, setCurrentZoom] = useState(17); 38 const isApiLoaded = useApiIsLoaded(); 39 const [searchTerm, setSearchTerm] = useState(""); 40 /* 41 const searchParams = useSearchParams(); 42 const search = searchParams.get("id"); 43 */ 44 const handleMarkerClick = (place: Place) => { 45 setSelectedPlace(place); 46 }; 47 const handleMapClick = (e: any) => { 48 if (!e.detail.placeId) { 49 setSelectedPlace(null); 50 } 51 }; 52 const handleZoomChange = (e: any) => { 53 setCurrentZoom(e.detail.zoom); 54 }; 55 56 const filteredPlaces = useMemo(() => { 57 if (!searchTerm) { 58 return places; 59 } 60 const lowerCaseSearch = searchTerm.toLowerCase(); 61 return places.filter( 62 (place) => 63 place.title.toLowerCase().includes(lowerCaseSearch) || 64 (typeof place.description === "string" && 65 place.description.toLowerCase().includes(lowerCaseSearch)), 66 ); 67 }, [searchTerm]); 68 /* 69 useEffect(() => { 70 if (search !== null) { 71 const searchNum = Number(search); 72 if (!isNaN(searchNum)) { 73 const foundPlace = findPlaceById(searchNum); 74 if (foundPlace) { 75 setSelectedPlace(foundPlace); 76 } 77 } 78 } 79 }, [search]); 80 */ 81 const [showAbout, setShowAbout] = useState(false); 82 83 useEffect(() => { 84 if ("geolocation" in navigator) { 85 navigator.geolocation.getCurrentPosition( 86 (position) => { 87 setUserLocation({ 88 lat: position.coords.latitude, 89 lng: position.coords.longitude, 90 }); 91 setLocationLoading(false); 92 }, 93 (error) => { 94 console.error("Geolocation Error:", error); 95 setLocationLoading(false); 96 }, 97 { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }, 98 ); 99 } else { 100 setLocationLoading(false); 101 } 102 }, []); 103 return ( 104 <div> 105 <main> 106 <header> 107 <p>島大松江 学祭マップ 2025</p> 108 </header> 109 <div> 110 </div> 111 <APIProvider apiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ""}> 112 <Map 113 style={{ width: "100vw", height: "calc(100svh - 100px)" }} 114 defaultCenter={{ lat: 35.4875, lng: 133.068315 }} 115 defaultZoom={17} 116 gestureHandling={"greedy"} 117 disableDefaultUI={true} 118 onClick={handleMapClick} 119 onZoomChanged={handleZoomChange} 120 > 121 {filteredPlaces.map((place) => ( 122 <Marker 123 key={place.id} 124 position={place.position} 125 onClick={() => handleMarkerClick(place)} 126 label={currentZoom > 19 ? markerLabel(place.title) : undefined} 127 /> 128 ))} 129 {userLocation && isApiLoaded && ( 130 <Marker 131 position={userLocation} 132 icon={{ 133 url: "data:image/svg+xml;utf-8,<svg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='10' fill='%231a73e8' stroke='%23ffffff' stroke-width='2'/></svg>", 134 scaledSize: new window.google.maps.Size(40, 40), 135 }} 136 /> 137 )} 138 </Map> 139 {selectedPlace && ( 140 <div className="table-container"> 141 <h2>{selectedPlace.title}</h2> 142 <div className="description-scroll"> 143 {selectedPlace.description} 144 </div> 145 </div> 146 )} 147 </APIProvider> 148 </main> 149 <footer> 150 <small> 151 <input type="text" placeholder="イベント名や説明で検索..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> 152 <br /> 153 <button className="about-link" onClick={() => setShowAbout(true)}> 154 About 155 </button> 156 © minerva_juppiter All Right Reserved 157 </small> 158 </footer> 159 {showAbout && ( 160 <div className="floating-modal"> 161 <div className="about">このサイトについて・免責事項</div> 162 <div> 163 <p> 164 1. 165 当サイトは非公式のマップであり,公式サイト,実行委員会,その他一切の関係団体とは関係ございません。 166 </p> 167 <p> 168 2. 169 当サイトに掲載する情報の正確性,完全性,最新性について,いかなる保証も行うものではありません。 170 </p> 171 <p> 172 3. 173 当サイトの利用により生じた損害または不利益について,当サイト運営者は一切の責任を負わないものとします。 174 </p> 175 <p> 176 4. 不具合・情報の誤り等がありましたら{" "} 177 <a 178 href="https://github.com/minerva-jupiter/shimane-u-fest-map" 179 target="_blank" 180 rel="noopener noreferrer" 181 > 182 開発リポジトリ 183 </a>{" "} 184 でissueを作成していただけますと幸いです。 185 </p> 186 </div> 187 <button 188 className="floating-modal-close" 189 onClick={() => setShowAbout(false)} 190 aria-label="Close" 191 > 192 × 193 </button> 194 </div> 195 )} 196 </div> 197 ); 198 }