diff --git a/devcon-app/precache.js b/devcon-app/precache.js index 08f768dfc..a95ed65c6 100644 --- a/devcon-app/precache.js +++ b/devcon-app/precache.js @@ -16,6 +16,11 @@ const pages = [ precacheHtml: true, // precacheJson: true, }, + { + route: '/event', + precacheHtml: true, + // precacheJson: true, + }, // { // route: '/side-events', // precacheHtml: true, diff --git a/devcon-app/src/assets/icons/dc-7/event-fill.svg b/devcon-app/src/assets/icons/dc-7/event-fill.svg new file mode 100644 index 000000000..cc8a995d3 --- /dev/null +++ b/devcon-app/src/assets/icons/dc-7/event-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/devcon-app/src/assets/icons/dc-7/event.svg b/devcon-app/src/assets/icons/dc-7/event.svg new file mode 100644 index 000000000..8a9edcf8a --- /dev/null +++ b/devcon-app/src/assets/icons/dc-7/event.svg @@ -0,0 +1,3 @@ + + + diff --git a/devcon-app/src/assets/images/dc-7/venue/qsncc.png b/devcon-app/src/assets/images/dc-7/venue/qsncc.png new file mode 100644 index 000000000..605a258ec Binary files /dev/null and b/devcon-app/src/assets/images/dc-7/venue/qsncc.png differ diff --git a/devcon-app/src/assets/images/dc-7/venue/venue-map.png b/devcon-app/src/assets/images/dc-7/venue/venue-map.png new file mode 100644 index 000000000..e202f2c04 Binary files /dev/null and b/devcon-app/src/assets/images/dc-7/venue/venue-map.png differ diff --git a/devcon-app/src/components/domain/app/Layout.tsx b/devcon-app/src/components/domain/app/Layout.tsx index 2eeb0b426..f87995208 100644 --- a/devcon-app/src/components/domain/app/Layout.tsx +++ b/devcon-app/src/components/domain/app/Layout.tsx @@ -1,6 +1,5 @@ import React, { PropsWithChildren, useState, useEffect, useRef, RefObject, ReactNode } from 'react' import css from './app.module.scss' -import useGetElementHeight from 'hooks/useGetElementHeight' import AppIcon from 'assets/icons/app-icons-1.svg' import TilesIcon from 'assets/icons/dc-7/tiles.svg' import TilesFillIcon from 'assets/icons/dc-7/tiles-fill.svg' @@ -23,6 +22,8 @@ import { useRecoilState } from 'recoil' import ChevronRightIcon from 'assets/icons/chevron_right.svg' import { devaBotVisibleAtom, selectedSessionAtom, sessionIdAtom, useSeenNotifications } from 'pages/_app' import { useAccountContext } from 'context/account-context' +import IconVenue from 'assets/icons/dc-7/event.svg' +import IconFilledVenue from 'assets/icons/dc-7/event-fill.svg' import ArrowBackIcon from 'assets/icons/arrow_left.svg' import { selectedSpeakerAtom } from 'pages/_app' @@ -326,6 +327,13 @@ const navItems = (isLoggedIn: boolean, pathname: string) => [ href: '/speakers', size: 18, }, + { + label: 'Event', + icon: pathname === '/event' ? IconFilledVenue : IconVenue, + href: '/event', + isActive: pathname.startsWith('/event'), + size: 18, + }, { icon: pathname.startsWith('/account') ? UserFillIcon : UserIcon, label: isLoggedIn ? 'Account' : 'Log In', @@ -333,12 +341,6 @@ const navItems = (isLoggedIn: boolean, pathname: string) => [ isActive: pathname.startsWith('/account'), size: 22, }, - // { - // icon: TicketIcon, - // label: 'Venue', - // href: '/venue', - // size: 18, - // }, ] export const useWindowWidth = () => { diff --git a/devcon-app/src/components/domain/app/dc7/event/event.module.scss b/devcon-app/src/components/domain/app/dc7/event/event.module.scss new file mode 100644 index 000000000..6da5cd23c --- /dev/null +++ b/devcon-app/src/components/domain/app/dc7/event/event.module.scss @@ -0,0 +1,35 @@ +.panzoom { + overflow: hidden; + position: relative; + height: min(60vh, 600px); + width: 100%; + cursor: grab; + + .image { + user-select: none; + width: 100%; + height: 100%; + max-height: 100%; + max-width: 100%; + position: relative; + overflow: hidden; + + + img { + object-fit: contain; + width: 100%; + max-height: 100%; + } + } + } + + .panzoom-cover { + @extend .panzoom; + cursor: auto; + + .image { + img { + object-fit: cover !important; + } + } + } \ No newline at end of file diff --git a/devcon-app/src/components/domain/app/dc7/event/index.tsx b/devcon-app/src/components/domain/app/dc7/event/index.tsx new file mode 100644 index 000000000..0248743e4 --- /dev/null +++ b/devcon-app/src/components/domain/app/dc7/event/index.tsx @@ -0,0 +1,232 @@ +import React from 'react' +import cn from 'classnames' +import ListIcon from 'assets/icons/list.svg' +import TimelineIcon from 'assets/icons/timeline.svg' +import VenueMap from 'assets/images/dc-7/venue/venue-map.png' +import VenueLogo from 'assets/images/dc-7/venue/qsncc.png' +import Image from 'next/image' +import css from './event.module.scss' +import { usePanzoom, PanzoomControls } from './panzoom' +import { StandalonePrompt } from 'lib/components/ai/standalone-prompt' +import { useRecoilState } from 'recoil' +import { devaBotVisibleAtom } from 'pages/_app' +import { Link } from 'components/common/link' + +// import Panzoom, { PanZoom } from 'panzoom' + +export const cardClass = + 'flex flex-col lg:border lg:border-solid lg:border-[#E4E6EB] rounded-3xl relative lg:bg-[#fbfbfb]' + +// const Switch = () => { +// return ( +//
+//
setTimelineView(false)} +// > +// +// Floor Map +//
+//
{ +// setTimelineView(true) + +// // if (Object.keys(sessionFilter.day).length === 0) { +// // setSessionFilter({ +// // ...sessionFilter, +// // day: { 'Nov 12': true }, +// // }) +// // } +// }} +// > +// +// Timeline View +//
+//
+// ) +// } + +const List = (props: any) => { + console.log(props) + + return ( +
+

Floors & Rooms

+ +
+ {/*

STAGES

*/} + {props.floors.map((floor: any) => { + let floorName = floor + + if (floor === 'G') floorName = 'G — Ground Floor' + if (floor === '1') floorName = 'L1 — First Floor' + if (floor === '2') floorName = 'L2 — Second Floor' + + const rooms = props.rooms + .filter((room: any) => room.info === floor) + .sort((a: any, b: any) => { + if (a.name === 'Main Stage') return -1 + if (b.name === 'Main Stage') return 1 + + if (a.name.toLowerCase().startsWith('stage')) { + if (b.name.toLowerCase().startsWith('stage')) { + return a.name.localeCompare(b.name) + } + return -1 + } + + if (b.name.toLowerCase().startsWith('stage')) return 1 + + if (a.name.toLowerCase().startsWith('breakout')) { + if (b.name.toLowerCase().startsWith('breakout')) { + return a.name.localeCompare(b.name) + } + return 1 + } + + if (b.name.toLowerCase().startsWith('breakout')) return -1 + + return a.name.localeCompare(b.name) + }) + + return ( +
+
+ {/*
*/} + {floorName} +
+ + {rooms.map((room: any) => { + const getColor = (roomName: string) => { + const name = roomName.toLowerCase() + if (name.startsWith('classroom')) return '#14B8A6' // teal + if (name.includes('decompression') || name.includes('music stage')) return '#22C55E' // green + if (name.startsWith('stage') || name === 'main stage') return '#7D52F4' // purple + if (name.startsWith('breakout')) return '#EF4444' // red + return '#7D52F4' // default purple + } + + console.log(room.name) + + let roomName = room.name + + if (roomName === 'Main Stage') { + roomName += ' — Masks' + } + + if (roomName === 'Stage 1') { + roomName += ' — Fans' + } + + if (roomName === 'Stage 2') { + roomName += ' — Lanterns' + } + + if (roomName === 'Stage 3') { + roomName += ' — Fabrics' + } + + if (roomName === 'Stage 4') { + roomName += ' — Leaf' + } + + if (roomName === 'Stage 5') { + roomName += ' — Hats' + } + + if (roomName === 'Stage 6') { + roomName += ' — Kites' + } + + if (roomName === 'Keynote') return null + + return ( + +
+
+ {roomName} +
+
+ Click to view sessions in this room +
+ + ) + })} +
+ ) + })} +
+
+ ) +} + +export const Venue = (props: any) => { + const pz = usePanzoom() + const [_, setDevaBotVisible] = useRecoilState(devaBotVisibleAtom) + + return ( + <> +
+
+
+ venue map +
+ +
+
QSNCC
+
Venue Map
+
+ +
+
Zoom/drag for a closer view
+
+
+
+
+ + +

Ask Devai

+ + setDevaBotVisible('Tell me where I can go to take a nap')}> +
Tell me where I can go to take a nap
+
+ setDevaBotVisible('What can I do at the music stage?')}> +
What can I do at the music stage?
+
+ setDevaBotVisible('What is the decompression room?')}> +
What is the decompression room?
+
+ setDevaBotVisible('What are breakout rooms?')}> +
What are breakout rooms?
+
+
+ + ) +} diff --git a/devcon-app/src/components/domain/app/dc7/event/panzoom.tsx b/devcon-app/src/components/domain/app/dc7/event/panzoom.tsx new file mode 100644 index 000000000..8a19629b7 --- /dev/null +++ b/devcon-app/src/components/domain/app/dc7/event/panzoom.tsx @@ -0,0 +1,287 @@ +import React from 'react' +// import { Link } from 'components/common/link' +import css from './event.module.scss' +import { Room } from 'types/Room' +// import { AppNav } from 'components/domain/app/navigation' +// import { Search } from 'components/common/filter/Filter' +// import filterCss from 'components/domain/app/app-filter.module.scss' +// import Image from 'next/image' +// import VenueMap from 'assets/images/venue-map/Venue.png' +import Panzoom, { PanZoom } from 'panzoom' +import IconPlus from 'assets/icons/plus.svg' +import IconMinus from 'assets/icons/minus.svg' +import { CircleIcon } from 'lib/components/circle-icon' +import { motion } from 'framer-motion' + +// import ListIcon from 'assets/icons/list.svg' +// import TileIcon from 'assets/icons/layers.svg' +// import { getFloorImage } from './Floor' +// import { defaultSlugify } from 'utils/formatting' +// import { RoomList } from './roomlist' +// import { CollapsedSection, CollapsedSectionContent, CollapsedSectionHeader } from 'components/common/collapsed-section' +// import { useIsStandalone } from 'utils/pwa-link' +// import imageAgora from './agora.png' +// import { Button } from 'components/common/button' +// import { useRouter } from 'next/router' +// import IconInformation from 'assets/icons/info.svg' +// import IconDirections from 'assets/icons/directions.svg' + +interface Props { + rooms: Array + floors: Array +} + +export const PanzoomControls = (props: { pz: PanZoom | null }) => { + const onClick = (e: any) => { + e.nativeEvent.preventDefault() + + const scene = document.getElementById('image-container') + + if (!scene || !props.pz) return + + const rect = scene.getBoundingClientRect() + // const cx = rect.x + rect.width / 2 + // const cy = rect.y + rect.height / 2 + const cx = scene.offsetLeft + rect.width / 2 + const cy = scene.offsetTop + rect.height / 2 + const isZoomIn = e.currentTarget.id === 'zoomIn' + const zoomBy = isZoomIn ? 2 : 0.5 + props.pz.smoothZoom(cx, cy, zoomBy) + } + + return ( +
+ {/* + + + + + */} +
+ +
+
+ +
+
+ ) + + return ( +
+
+
+ +
+
+ +
+
+
+ ) +} + +export const usePanzoom = () => { + const [panzoomInstance, setPanzoomInstance] = React.useState(null) + + React.useEffect(() => { + const scene = document.getElementById('venue-image') + + if (scene) { + const panzoomInstance = Panzoom(scene, { + bounds: true, + boundsPadding: 0.1, + // maxZoom: 1, + minZoom: 0.5, + beforeWheel: function (e) { + // allow wheel-zoom only if altKey is down. Otherwise - ignore + var shouldIgnore = !e.ctrlKey + return shouldIgnore + }, + }) + + setPanzoomInstance(panzoomInstance) + + return () => { + setPanzoomInstance(null) + panzoomInstance.dispose() + } + } + }, []) + + return panzoomInstance +} + +// export const Venue = (props: Props) => { +// const router = useRouter() +// const isStandalone = useIsStandalone() +// const [openFloors, setOpenFloors] = React.useState({} as { [key: string]: boolean }) +// const [listView, setListView] = React.useState(false) +// const [search, setSearch] = React.useState('') + +// const filteredFloors = ( +// search +// ? props.floors.filter(floor => { +// if (floor.toLowerCase().includes(search.toLowerCase())) return true + +// const roomsByFloor = props.rooms.filter(i => i.info === floor) + +// return roomsByFloor.some( +// room => room.name.toLowerCase().includes(search) || room.description.toLowerCase().includes(search) +// ) +// }) +// : props.floors +// ).sort((a, b) => b.localeCompare(a)) +// const basement = filteredFloors.shift() +// if (basement) filteredFloors.push(basement) + +// function onSearch(nextVal: any) { +// setSearch(nextVal) + +// if (!nextVal) { +// setOpenFloors({}) +// } else { +// filteredFloors.forEach(floor => +// setOpenFloors(openFloors => { +// return { +// ...openFloors, +// [floor]: true, +// } +// }) +// ) +// } +// } + +// return ( +// <> +// { +// return ( +// <> +// +// +// + +// +// +// +// +// ) +// }} +// /> + +// {/*
+//
+// venue map +//
+//
*/} + +//
+//
+//
+// + +//
+// +// +//
+//
+//
+//
+ +//
+// {/*

Floors

*/} + +//
+//
+//

Agora Bogotá Convention Center

+// +//
+//
+// Agora Bogotá Convention Center +//
+//
+ +// {listView && +// filteredFloors.map(floor => { +// const roomsByFloor = props.rooms.filter(i => i.info === floor) + +// return ( +// { +// const isOpen = openFloors[floor] +// const nextOpenState = { +// ...openFloors, +// [floor]: true, +// } + +// if (isOpen) { +// delete nextOpenState[floor] +// } + +// setOpenFloors(nextOpenState) +// }} +// > +// +//

{floor}

+//
+// +//
+// +//
+//
+//
+// ) +// })} + +// {!listView && +// filteredFloors.map(floor => { +// return ( +// +//
{floor}
+//
{getFloorImage(floor, 'fill')}
+// +// ) +// })} +//
+// +// ) +// } diff --git a/devcon-app/src/components/domain/app/dc7/sessions/index.tsx b/devcon-app/src/components/domain/app/dc7/sessions/index.tsx index c7e959ef1..85671dca5 100644 --- a/devcon-app/src/components/domain/app/dc7/sessions/index.tsx +++ b/devcon-app/src/components/domain/app/dc7/sessions/index.tsx @@ -38,6 +38,7 @@ import CollapsedIcon from 'assets/icons/collapsed.svg' import ExpandedIcon from 'assets/icons/expanded.svg' import { devaBotVisibleAtom, + initialFilterState, selectedSessionAtom, selectedSessionSelector, sessionFilterAtom, @@ -108,7 +109,7 @@ const useSessionFilter = (sessions: SessionType[], event: any) => { if (typeof window === 'undefined') return const searchParams = new URLSearchParams(window.location.search) - const newFilter = { ...sessionFilter } + const newFilter = { ...initialFilterState } as any //...sessionFilter } searchParams.forEach((value, key) => { if (key in newFilter) { diff --git a/devcon-app/src/pages/_app.tsx b/devcon-app/src/pages/_app.tsx index 5464e21da..12927994f 100644 --- a/devcon-app/src/pages/_app.tsx +++ b/devcon-app/src/pages/_app.tsx @@ -21,6 +21,11 @@ import { Toaster } from 'lib/components/ui/toaster' import { usePathname } from 'next/navigation' import { DataProvider } from 'context/data' +export const selectedEventTabAtom = atom<'venue' | 'information' | 'contact' | 'directions'>({ + key: 'selectedEventTab', + default: 'venue', +}) + // This selector is used to get the full speaker object from the selectedSpeakerAtom - useful for /speakers pages where the full object is needed - this can be impartial if the speaker was linked from a session (where the speakers don't recursively have the session objects) export const selectedSpeakerSelector = selector({ key: 'selectedSpeakerSelector', diff --git a/devcon-app/src/pages/event/index.tsx b/devcon-app/src/pages/event/index.tsx new file mode 100644 index 000000000..65a7a9e8b --- /dev/null +++ b/devcon-app/src/pages/event/index.tsx @@ -0,0 +1,96 @@ +import { AppLayout } from 'components/domain/app/Layout' +import React from 'react' +import { fetchEvent, fetchRooms, fetchSessionsByRoom } from 'services/event-data' +import { SEO } from 'components/domain/seo' +import cn from 'classnames' +import { useRecoilValue, useRecoilState } from 'recoil' +import { selectedEventTabAtom } from 'pages/_app' +import { Venue } from 'components/domain/app/dc7/event' + +const activeClass = '!border-[#7D52F4] !text-[#7D52F4] ' +const tabClass = + 'cursor-pointer pb-2 px-0.5 border-b-2 border-solid border-transparent transition-all duration-300 select-none' + +const Tabs = () => { + const [selectedEventTab, setSelectedEventTab] = useRecoilState(selectedEventTabAtom) + + // return null + + return ( +
+
setSelectedEventTab('venue')} + className={cn(tabClass, selectedEventTab === 'venue' && activeClass)} + > + Venue Map +
+ {/*
setSelectedEventTab('information')} + className={cn(tabClass, selectedEventTab === 'information' && activeClass)} + > + Information +
*/} + {/*
setSelectedEventTab('contact')} + className={cn(tabClass, selectedEventTab === 'contact' && activeClass)} + > + Contact +
+
setSelectedEventTab('directions')} + className={cn(tabClass, selectedEventTab === 'directions' && activeClass)} + > + Directions +
*/} +
+ ) +} + +const VenuePage = (props: any) => { + const floorOrder: any = { G: 0, '1': 1, '2': 2 } + const uniqueFloors = [...new Set(props.rooms.map((room: any) => room.info))].sort( + (a: any, b: any) => floorOrder[a] - floorOrder[b] + ) + + return ( + + + + + + ) +} + +export default VenuePage + +// export async function getStaticPaths() { +// const rooms = await fetchRooms() +// const paths = rooms.map(i => { +// return { params: { id: i.id } } +// }) + +// return { +// paths, +// fallback: false, +// } +// } + +export async function getStaticProps(context: any) { + // const id = context.params.id + // const room = (await fetchRooms()).find(i => i.id === id) + + // if (!room) { + // return { + // props: null, + // notFound: true, + // } + // } + + return { + props: { + event: await fetchEvent(), + rooms: await fetchRooms(), + // sessions: await fetchSessionsByRoom(id), + }, + } +} diff --git a/devcon-app/src/pages/venue/room b/devcon-app/src/pages/venue/room deleted file mode 100644 index 033431bbe..000000000 --- a/devcon-app/src/pages/venue/room +++ /dev/null @@ -1,50 +0,0 @@ -import { AppLayout } from 'components/domain/app/Layout' -import { Room } from 'components/domain/app/venue' -import React from 'react' -import { fetchEvent, fetchRooms, fetchSessionsByRoom } from 'services/event-data' -import { DEFAULT_APP_PAGE } from 'utils/constants' -import { SEO } from 'components/domain/seo' - -const VenuePage = (props: any) => { - return ( - - - - - ) -} - -export default VenuePage - -export async function getStaticPaths() { - const rooms = await fetchRooms() - const paths = rooms.map(i => { - return { params: { id: i.id } } - }) - - return { - paths, - fallback: false, - } -} - -export async function getStaticProps(context: any) { - const id = context.params.id - const room = (await fetchRooms()).find(i => i.id === id) - - if (!room) { - return { - props: null, - notFound: true, - } - } - - return { - props: { - page: DEFAULT_APP_PAGE, - event: await fetchEvent(), - room, - sessions: await fetchSessionsByRoom(id), - }, - } -} \ No newline at end of file diff --git a/lib/components/circle-icon/index.tsx b/lib/components/circle-icon/index.tsx index ec5a345e0..98c84fef7 100644 --- a/lib/components/circle-icon/index.tsx +++ b/lib/components/circle-icon/index.tsx @@ -20,6 +20,7 @@ export const CircleIcon = (props: any) => { const body = (