Skip to content

Commit

Permalink
Initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
malted committed Jan 16, 2025
1 parent be383f8 commit 8b46a0c
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 5 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": "^5.0.0",
"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
5 changes: 0 additions & 5 deletions src/app/harbor/map/map.tsx

This file was deleted.

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

import dynamic from 'next/dynamic'

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

export default function MapParent(props) {
return <DynamicMap {...props} />
}
140 changes: 140 additions & 0 deletions src/app/harbor/tavern/map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Head from 'next/head'

import { useEffect, useState } from 'react'
import {
getTavernPeople,
getTavernEvents,
type TavernPersonItem,
type TavernEventItem,
} from './tavern-utils'
import MapParent from './map-parent'
import { type LatLngExpression, DivIcon, Icon } from 'leaflet'
import { MapContainer, TileLayer, Marker, useMap, Tooltip } from 'react-leaflet'

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

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

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

return (
<div>
{/* <Head>
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossOrigin=""
/>
</Head> */}

{/* <MapParent tavernPeople={tavernPeople} tavernEvents={tavernEvents}>
<MapContainer
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>
</MapParent> */}
</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) => {
const iconClass = `tavern-marker tavern-${!t.status ? 'default' : t.status}`

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) => {
let iconUrl
if (e.organizers.length === 0) {
iconUrl = '/handraise.png'
} else {
iconUrl = '/tavern.png'
}

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

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

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

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[]
}
68 changes: 68 additions & 0 deletions src/app/harbor/tavern/tavern-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'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 = 5 * 60 * 1000

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

console.log('Fetching tavern people')
lastPeopleFetch = Date.now()
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()

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

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

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

return records.map((r) => ({
id: r.id,
city: r.get('city'),
geocode: r.get('map_geocode'),
organizers: r.get('organizers') ?? [],
})) as TavernEventItem[]
}
2 changes: 2 additions & 0 deletions src/app/harbor/tavern/tavern.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect } from 'react'
import useLocalStorageState from '../../../../lib/useLocalStorageState'
import { setTavernRsvpStatus, getTavernRsvpStatus } from '@/app/utils/tavern'
import { Card } from '@/components/ui/card'
import Map from './map'

const RsvpStatusSwitcher = () => {
const [rsvpStatus, setRsvpStatus] = useLocalStorageState(
Expand Down Expand Up @@ -45,6 +46,7 @@ export default function Tavern() {
<h1 className="font-heading text-5xl mb-6 text-center relative w-fit mx-auto">
Mystic Tavern
</h1>
<Map />
<Card className="mb-8 p-6">
<p className="mb-4">
On January 31st, thousands of ships will sail back to port,
Expand Down

0 comments on commit 8b46a0c

Please sign in to comment.