Skip to content

Commit

Permalink
Merge pull request #1122 from hackclub/malted/rsvp-specifics-2
Browse files Browse the repository at this point in the history
RSVP to a specific tavern event
  • Loading branch information
malted authored Jan 22, 2025
2 parents 8e3dd78 + ab6b806 commit 7f29e11
Showing 4 changed files with 237 additions and 55 deletions.
41 changes: 32 additions & 9 deletions src/app/harbor/tavern/map.tsx
Original file line number Diff line number Diff line change
@@ -12,24 +12,34 @@ import { MapContainer, TileLayer, Marker, useMap, Tooltip } from 'react-leaflet'
import 'leaflet/dist/leaflet.css'
import { Card } from '@/components/ui/card'

const MAP_ZOOM = 2,
MAP_CENTRE: LatLngExpression = [0, 0]
export default function Map({ tavernEvents, tavernPeople, selectedTavern }) {
const [mapInstance, setMapInstance] = useState(null)

useEffect(() => {
if (selectedTavern && selectedTavern.geocode && mapInstance) {
const geocodeData = JSON.parse(
atob(selectedTavern.geocode.slice(2).trim()),
)
if (geocodeData.o.status === 'OK') {
mapInstance.setView([geocodeData.o.lat, geocodeData.o.lng], 11)
}
}
}, [selectedTavern, mapInstance])

export default function Map({ tavernEvents, tavernPeople }) {
return (
<div>
<MapContainer
className="h-96 rounded-lg"
center={MAP_CENTRE}
zoom={MAP_ZOOM}
center={[0, 0]}
zoom={2}
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 />
<MapUpdater selectedTavern={selectedTavern} />
</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>
@@ -72,18 +82,31 @@ export default function Map({ tavernEvents, tavernPeople }) {
)
}

function UserLocation() {
function MapUpdater({
selectedTavern,
}: {
selectedTavern: TavernEventItem | null
}) {
const map = useMap()

useEffect(() => {
if (navigator.geolocation) {
if (!map) return

if (selectedTavern && selectedTavern.geocode) {
const geocodeData = JSON.parse(
atob(selectedTavern.geocode.slice(2).trim()),
)
if (geocodeData.o.status === 'OK') {
map.setView([geocodeData.o.lat, geocodeData.o.lng], 11)
}
} else if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((loc) => {
if (map !== null) {
map.setView([loc.coords.latitude, loc.coords.longitude], 11)
}
})
}
}, [map])
}, [selectedTavern, map])

return null
}
12 changes: 11 additions & 1 deletion src/app/harbor/tavern/tavern-utils.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,8 @@ export type TavernEventItem = {
id: string
city: string
geocode: string
locality: string
attendeeCount: number
organizers: string[]
}

@@ -56,15 +58,23 @@ export const getTavernEvents = async () => {
const base = Airtable.base(process.env.BASE_ID!)
const records = await base('taverns')
.select({
fields: ['city', 'map_geocode', 'organizers'],
fields: [
'city',
'map_geocode',
'organizers',
'locality',
'attendees_count',
],
})
.all()

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

cachedEvents = items
165 changes: 122 additions & 43 deletions src/app/harbor/tavern/tavern.tsx
Original file line number Diff line number Diff line change
@@ -6,6 +6,9 @@ import {
setTavernRsvpStatus,
getTavernRsvpStatus,
submitMyTavernLocation,
getMyTavernLocation,
submitShirtSize,
getShirtSize,
} from '@/app/utils/tavern'
import { Card } from '@/components/ui/card'
import dynamic from 'next/dynamic'
@@ -15,12 +18,14 @@ import {
TavernEventItem,
TavernPersonItem,
} from './tavern-utils'
import Modal from '@/components/ui/modal'
import { Button } from '@/components/ui/button'

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

const RsvpStatusSwitcher = ({ tavernEvents }) => {
const RsvpStatusSwitcher = ({ tavernEvents, onTavernSelect }) => {
const [rsvpStatus, setRsvpStatus] = useLocalStorageState(
'cache.rsvpStatus',
'none',
@@ -29,75 +34,142 @@ const RsvpStatusSwitcher = ({ tavernEvents }) => {
'cache.whichTavern',
'none',
)
const [shirtSize, setShirtSize] = useLocalStorageState(
'cache.shirtSize',
'none',
)
const [attendeeNoOrganizerModal, setAttendeeNoOrganizerModal] =
useState(false)

useEffect(() => {
// set rsvp status
getTavernRsvpStatus().then((status) => setRsvpStatus(status))
getShirtSize().then((ss) => setShirtSize(ss))
}, [])

const onOptionChangeHandler = (e) => {
setRsvpStatus(e.target.value)
setTavernRsvpStatus(e.target.value)
const status = e.target.value
setRsvpStatus(status)
setTavernRsvpStatus(status)

if (e.target.value !== 'participant') {
if (status !== 'participant' && status !== 'organizer') {
setWhichTavern('none')
submitMyTavernLocation(null)
onTavernSelect(null)
}
}

const onTavernChangeHandler = (event) => {
const tavernId = event.target.value
setWhichTavern(tavernId)
submitMyTavernLocation(tavernId).catch(console.error)
onTavernSelect(tavernId)

if (
rsvpStatus === 'participant' &&
tavernEvents.find((te) => te.id === tavernId).organizers.length === 0
) {
console.log('u shoiuld vhe an organizer')
setAttendeeNoOrganizerModal(true)
}
}

return (
<div className="text-center mb-6 mt-12" id="region-select">
<label>Will you join?</label>
<select
onChange={onOptionChangeHandler}
value={rsvpStatus}
className="ml-2 text-gray-600 rounded-sm"
<>
<Modal
isOpen={attendeeNoOrganizerModal}
close={() => setAttendeeNoOrganizerModal(false)}
>
<option disabled>Select</option>
<option value="none">Nope, can't do either</option>
<option value="organizer">I can organize a tavern near me</option>
<option value="participant">I want to attend a tavern near me</option>
</select>

{tavernEvents && rsvpStatus === 'participant' ? (
<div>
<label>Which tavern will you attend?</label>
<select
onChange={async () => {
setWhichTavern(event.target.value)
await submitMyTavernLocation(event.target.value)
}}
value={whichTavern}
className="ml-2 text-gray-600 rounded-sm"
>
<option value="">Select</option>
{tavernEvents.map((te, idx) => (
<option key={idx} value={te.id}>
{JSON.parse(atob(te.geocode.substring(3))).i}
ARRRRR! There are enough pirates here to host a tavern, but nobody has
volunteered to organize.
<br />
<br />
It's easy, all you would need to do is select a venue, date, distribute
shirts, and help peolpe coordianate how they're going to attend.
<br />
<br />
Please consider volunteering to organize this tavern, me hearty!
</Modal>
<div className="text-center mb-6 mt-12" id="region-select">
<label>Will you join?</label>
<select
onChange={onOptionChangeHandler}
value={rsvpStatus}
className="ml-2 text-gray-600 rounded-sm"
>
<option disabled>Select</option>
<option value="none">Nope, can't do either</option>
<option value="organizer">I can organize a tavern near me</option>
<option value="participant">I want to attend a tavern near me</option>
</select>

{tavernEvents &&
(rsvpStatus === 'participant' || rsvpStatus === 'organizer') ? (
<div>
<label>Which tavern will you attend?</label>
<select
onChange={onTavernChangeHandler}
value={whichTavern}
className="ml-2 text-gray-600 rounded-sm"
>
<option value="" disabled>
Select
</option>
))}
</select>
</div>
) : (
<p>Loading...</p>
)}
</div>
{tavernEvents.map((te, idx) => (
<option key={idx} value={te.id}>
{te.locality}
{te.organizers.length === 0 ? ' (no organizers yet!)' : ''}
</option>
))}
</select>

<label>What is your shirt size?</label>
<select
onChange={async (e) => {
setShirtSize(e.target.value)
await submitShirtSize(e.target.value)
}}
value={shirtSize}
className="ml-2 text-gray-600 rounded-sm"
>
<option disabled>Select</option>
<option value="Small">Small</option>
<option value="Medium">Medium</option>
<option value="Large">Large</option>
<option value="XL">XL</option>
<option value="XXL">XXL</option>
</select>
</div>
) : null}
</div>
</>
)
}

export default function Tavern() {
const [tavernPeople, setTavernPeople] = useState<TavernPersonItem[]>([])
const [tavernEvents, setTavernEvents] = useState<TavernEventItem[]>([])
const [selectedTavern, setSelectedTavern] = useState<TavernEventItem | null>(
null,
)

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

console.log({ te })
setSelectedTavern(myTavernLocation)
})
}, [])

const handleTavernSelect = (tavernId: string | null) => {
const tavern = tavernEvents.find((te) => te.id === tavernId) || null
setSelectedTavern(tavern)
}

return (
<div className="container mx-auto px-4 py-8 text-white relative">
<div className="text-white">
@@ -151,9 +223,16 @@ export default function Tavern() {
😉
</p>
</Card>
<RsvpStatusSwitcher tavernEvents={tavernEvents} />
<RsvpStatusSwitcher
tavernEvents={tavernEvents}
onTavernSelect={handleTavernSelect}
/>

<Map tavernEvents={tavernEvents} tavernPeople={tavernPeople} />
<Map
tavernEvents={tavernEvents}
tavernPeople={tavernPeople}
selectedTavern={selectedTavern}
/>
</div>
</div>
)
Loading

0 comments on commit 7f29e11

Please sign in to comment.