Skip to content

Commit

Permalink
Merge pull request #1105 from hackclub/malted/tavern-map
Browse files Browse the repository at this point in the history
Add tavern map to shipyard
  • Loading branch information
malted authored Jan 18, 2025
2 parents cb1957c + d5372c1 commit 879f91d
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 39 deletions.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"javascript-time-ago": "^2.5.11",
"js-confetti": "^0.12.0",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"lucide-react": "^0.446.0",
"next": "14.2.15",
"next-plausible": "^3.12.2",
Expand All @@ -48,6 +49,7 @@
"react-fast-marquee": "^1.6.5",
"react-fullstory": "^1.4.0",
"react-infinite-scroll-component": "^6.1.0",
"react-leaflet": "^4.2.1",
"react-markdown": "^9.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
Expand All @@ -58,6 +60,7 @@
"uuid": "^11.0.3"
},
"devDependencies": {
"@types/leaflet": "^1.9.16",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
Binary file added public/handraise.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/tavern.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions src/app/harbor/map/map.tsx

This file was deleted.

186 changes: 186 additions & 0 deletions src/app/harbor/tavern/map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
'use client'

import { useEffect, useState } from 'react'
import {
getTavernPeople,
getTavernEvents,
type TavernPersonItem,
type TavernEventItem,
} from './tavern-utils'
import { type LatLngExpression, DivIcon, Icon } from 'leaflet'
import { MapContainer, TileLayer, Marker, useMap, Tooltip } from 'react-leaflet'
import 'leaflet/dist/leaflet.css'
import { Card } from '@/components/ui/card'

const MAP_ZOOM = 11,
MAP_CENTRE: LatLngExpression = [0, 0]

export default function Map() {
const [tavernPeople, setTavernPeople] = useState<TavernPersonItem[]>([])
const [tavernEvents, setTavernEvents] = useState<TavernEventItem[]>([])

useEffect(() => {
Promise.all([getTavernPeople(), getTavernEvents()]).then(([tp, te]) => {
setTavernPeople(tp)
setTavernEvents(te)
})
}, [])

return (
<div>
<MapContainer
className="h-96 rounded-lg"
center={MAP_CENTRE}
zoom={MAP_ZOOM}
scrollWheelZoom={false}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
/>
<TavernMarkers people={tavernPeople} events={tavernEvents} />
<UserLocation />
</MapContainer>
<Card className="mt-8 p-3 flex flex-row justify-center items-center gap-5 flex-wrap">
<p className="w-full text-center">Map Legend</p>
<div className="flex flex-row justify-start items-center gap-2">
<img
src="/tavern.png"
alt="a star representing a tavern"
width={20}
className="inline-block ml-4 hidden sm:inline"
/>
<p>Mystic Tavern</p>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<img
src="/handraise.png"
alt="someone raising a hand"
width={20}
className="inline-block ml-4 hidden sm:inline"
/>
<p>Mystic Tavern without organizer</p>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<div className="h-7 w-7 rounded-full border-2 border-white bg-[#cfdfff]"></div>
<p>Someone unable to organize or attend</p>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<div className="h-7 w-7 rounded-full border-2 border-white bg-[#ffd66e]"></div>
<p>Someone able to organize</p>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<div className="h-7 w-7 rounded-full border-2 border-white bg-[#f82b60]"></div>
<p>Someone able to attend</p>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<div className="h-7 w-7 rounded-full border-2 border-white bg-[#666666]"></div>
<p>Someone who has not responded</p>
</div>
</Card>
</div>
)
}

function UserLocation() {
const map = useMap()

useEffect(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((loc) => {
if (map !== null) {
map.setView([loc.coords.latitude, loc.coords.longitude], 11)
}
})
}
}, [map])

return null
}

function TavernMarkers(props: MapProps) {
const map = useMap()

if (!map) return null

const peopleMarkers = props.people.map((t) => {
let iconClass = `rounded-full border-2 border-white w-full h-full `

switch (t.status) {
case 'none': {
iconClass += 'bg-[#cfdfff]'
break
}
case 'organizer': {
iconClass += 'bg-[#ffd66e]'
break
}
case 'participant': {
iconClass += 'bg-[#f82b60]'
break
}
default: {
iconClass += 'bg-[#666666]'
break
}
}

const icon = new DivIcon({
className: iconClass,
iconSize: [25, 25],
})

return (
<Marker
key={t.id}
position={
t.coordinates.split(', ').map((c) => Number(c)) as LatLngExpression
}
icon={icon}
/>
)
})
const eventMarkers = props.events
.map((e) => {
if (!e.geocode) {
return null
}

const geocodeObj = JSON.parse(atob(e.geocode.slice(2).trim()))

if (geocodeObj.o.status !== 'OK') {
return null
}

let iconUrl
if (e.organizers.length === 0) {
iconUrl = '/handraise.png'
} else {
iconUrl = '/tavern.png'
}

const icon = new Icon({
iconUrl,
iconSize: [50, 50],
})

return (
<Marker
key={e.id}
position={[geocodeObj.o.lat, geocodeObj.o.lng]}
icon={icon}
zIndexOffset={20}
>
<Tooltip>{e.city}</Tooltip>
</Marker>
)
})
.filter((e) => e !== null)

return [...peopleMarkers, ...eventMarkers]
}

type MapProps = {
people: TavernPersonItem[]
events: TavernEventItem[]
}
75 changes: 75 additions & 0 deletions src/app/harbor/tavern/tavern-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use server'

import Airtable from 'airtable'

Airtable.configure({
apiKey: process.env.AIRTABLE_API_KEY,
endpointUrl: process.env.AIRTABLE_ENDPOINT_URL,
})

type RsvpStatus = 'none' | 'organizer' | 'participant'
export type TavernPersonItem = {
id: string
status: RsvpStatus
coordinates: string
}
export type TavernEventItem = {
id: string
city: string
geocode: string
organizers: string[]
}

let cachedPeople: TavernPersonItem[] | null,
cachedEvents: TavernEventItem[] | null
let lastPeopleFetch = 0,
lastEventsFetch = 0
const TTL = 30 * 60 * 1000

export const getTavernPeople = async () => {
if (Date.now() - lastPeopleFetch < TTL) return cachedPeople

console.log('Fetching tavern people')
const base = Airtable.base(process.env.BASE_ID!)
const records = await base('people')
.select({
fields: ['tavern_rsvp_status', 'tavern_map_coordinates'],
filterByFormula:
'AND({tavern_map_coordinates} != "", OR(tavern_rsvp_status != "", shipped_ship_count >= 1))',
})
.all()

const items = records.map((r) => ({
id: r.id,
status: r.get('tavern_rsvp_status'),
coordinates: r.get('tavern_map_coordinates'),
})) as TavernPersonItem[]

cachedPeople = items
lastPeopleFetch = Date.now()

return items
}

export const getTavernEvents = async () => {
if (Date.now() - lastEventsFetch < TTL) return cachedEvents

console.log('Fetching tavern events')
const base = Airtable.base(process.env.BASE_ID!)
const records = await base('taverns')
.select({
fields: ['city', 'map_geocode', 'organizers'],
})
.all()

const items = records.map((r) => ({
id: r.id,
city: r.get('city'),
geocode: r.get('map_geocode'),
organizers: r.get('organizers') ?? [],
})) as TavernEventItem[]

cachedEvents = items
lastEventsFetch = Date.now()
return items
}
42 changes: 8 additions & 34 deletions src/app/harbor/tavern/tavern.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { useEffect } from 'react'
import useLocalStorageState from '../../../../lib/useLocalStorageState'
import { setTavernRsvpStatus, getTavernRsvpStatus } from '@/app/utils/tavern'
import { Card } from '@/components/ui/card'
import dynamic from 'next/dynamic'

const Map = dynamic(() => import('./map'), {
ssr: false,
})

const RsvpStatusSwitcher = () => {
const [rsvpStatus, setRsvpStatus] = useLocalStorageState(
Expand Down Expand Up @@ -45,40 +50,7 @@ export default function Tavern() {
<h1 className="font-heading text-5xl mb-6 text-center relative w-fit mx-auto">
Mystic Tavern
</h1>
{/* <div className="mb-4 rounded-lg overflow-clip">
<iframe
src="https://high-seas-tavern-map.vercel.app"
allow="geolocation 'self' https://highseas.hackclub.com"
className="w-full h-96"
/>
<style>{`
.tavern-organizer {
background-color: #ffd66e;
}
.tavern-participant {
background-color: #f82b60;
}
.tavern-none {
background-color: #cfdfff;
}
.tavern-default {
background-color: #666666;
}
`}</style>
<div className="w-fit ml-auto flex">
<p className="px-2">легенда:</p>
<p className="tavern-organizer px-2 text-black">Organiser</p>
<p className="tavern-participant px-2">Participant</p>
<p className="tavern-none px-2 text-black">Can not go</p>
<p className="tavern-default px-2">Unresponded</p>
</div>
</div> */}

<Card className="mb-8 p-6">
<Card className="my-8 p-6">
<p className="mb-4">
On January 31st, thousands of ships will sail back to port,
weathered and weary from their months-long voyage upon the High
Expand Down Expand Up @@ -126,6 +98,8 @@ export default function Tavern() {
</p>
</Card>
<RsvpStatusSwitcher />

<Map />
</div>
</div>
)
Expand Down

0 comments on commit 879f91d

Please sign in to comment.