Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hub Map - office visits, room reservations, events #4

Merged
merged 10 commits into from
Mar 4, 2024
4 changes: 4 additions & 0 deletions config-demo/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
"id": "news",
"enabled": true
},
{
"id": "hub-map",
"enabled": true
},
{
"id": "profile-questions",
"enabled": true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"pino-pretty": "^8.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-easy-panzoom": "^0.4.4",
"react-query": "^3.39.1",
"react-table": "^7.8.0",
"sequelize": "^6.29.0",
Expand Down
2 changes: 1 addition & 1 deletion src/client/components/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const Tabs: React.FC<{
[]
)
return (
<div className="bg-bg-primary flex rounded-b-sm px-4 mb-1 md:mb-2">
<div className="bg-bg-primary flex rounded-sm px-4 mb-1 md:mb-2">
{TABS.map((tab) => {
const IconComponent = Icons[tab.icon as Icon]
return (
Expand Down
247 changes: 191 additions & 56 deletions src/client/components/OfficeFloorMap.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,208 @@
import React from 'react'
import { Button } from '#client/components/ui'
import { OfficeArea } from '#shared/types'
import React, { MouseEventHandler } from 'react'
import { Avatar, Button, P } from '#client/components/ui'
import {
ScheduledItemType,
OfficeArea,
OfficeAreaDesk,
OfficeRoom,
VisitType,
} from '#shared/types'
import { cn } from '#client/utils'
import { useStore } from '@nanostores/react'
import * as stores from '#client/stores'
import { ImageWithPanZoom } from './ui/ImageWithPanZoom'

type PointComponentFunctionProps = (
item: OfficeAreaDesk | OfficeRoom,
isSelected: boolean,
isAvailable: boolean,
onClick: (id: string, kind: string) => MouseEventHandler<HTMLAnchorElement>
) => Element | JSX.Element

const pointCommonStyle =
'rounded-sm border-2 -translate-y-1/2 -translate-x-1/2 hover:scale-105 transition-all delay-100 '

const PointComponent: Record<
VisitType.Visit | VisitType.RoomReservation,
PointComponentFunctionProps
> = {
[VisitType.Visit]: (item, isSelected, isAvailable, onClick) => (
<Button
size="small"
kind={isSelected ? 'primary' : 'secondary'}
disabled={!isAvailable}
color={isSelected ? 'purple' : 'default'}
className={cn(
pointCommonStyle,
isSelected
? 'border-pink-600 bg-accents-pink'
: 'bg-violet-300 border-violet-300',
isAvailable
? 'hover:bg-cta-purple hover:border-cta-hover-purpleNoOpacity hover:text-white'
: 'hover:scale-100 hover:bg-violet-300',
'min-h-[32px] min-w-[32px]'
)}
onClick={onClick(item.id, VisitType.Visit)}
>
{item?.name}
</Button>
),
[VisitType.RoomReservation]: (item, isSelected, isAvailable, onClick) => (
<Button
size="small"
kind={isSelected ? 'primary' : 'secondary'}
disabled={!isAvailable}
className={cn(
isSelected
? 'border-pink-600 hover:text-white bg-accents-pink hover:bg-accents-pinkDark'
: 'text-black bg-green-200 border-green-200 hover:bg-cta-jade hover:border-cta-hover-jadeNoOpacity hover:text-white',
'sm:p-4',
pointCommonStyle
)}
onClick={onClick(item.id, VisitType.RoomReservation)}
>
<p className="font-bold">{item.name}</p>
</Button>
),
}

type OfficeFloorMapProps = {
area: OfficeArea
availableDeskIds: string[]
selectedDeskId: string | null
onToggleDesk: (deskId: string) => void
mappablePoints?: Array<any>
panZoom?: boolean
officeVisits?: Record<string, Array<ScheduledItemType>>
showUsers?: boolean
selectedPointId: string | null
clickablePoints?: string[]
onToggle: (id: string, kind: string) => void
}

export const OfficeFloorMap: React.FC<OfficeFloorMapProps> = ({
area,
availableDeskIds,
selectedDeskId,
onToggleDesk,
mappablePoints,
panZoom = false,
officeVisits,
showUsers = false,
selectedPointId,
clickablePoints,
onToggle,
}) => {
const me = useStore(stores.me)
const initialStartPosition = selectedPointId
? mappablePoints?.find(
(point: ScheduledItemType) => point.id === selectedPointId
)
: null

const onClick = React.useCallback(
(deskId: string) => (ev: React.MouseEvent<HTMLButtonElement>) => {
(id: string, kind: string) => (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault()
onToggleDesk(deskId)
onToggle(id, kind)
},
[onToggleDesk]
[onToggle]
)

// @todo fix types here
const mapObjects = (scale: number) =>
!mappablePoints
? []
: mappablePoints
.filter((x) => x.position)
.map((x) => {
let isSelected = selectedPointId === x.id
let isAvailable = false
let user = null

if (!!officeVisits && me && showUsers) {
const bookedVisit: ScheduledItemType | undefined =
officeVisits.visit?.find(
(v) => v.areaId === area.id && v.objectId === x.id
)
if (!!bookedVisit) {
if (bookedVisit?.user?.id === me?.id) {
user = me
} else {
user = bookedVisit.user
}
}
}
isSelected = selectedPointId === x.id
isAvailable = !!clickablePoints?.includes(x.id)

const style = {
left: `${x.position?.x}%`,
top: `${x.position?.y}%`,
transform: `scale(${1 / scale})`,
transformOrigin: 'top left',
}

if (!!user && !!me) {
return (
<a
href={`/profile/${user.id}`}
className="absolute -translate-y-1/2 -translate-x-1/2"
style={style}
key={user.id + x.position.x + x.position.y}
>
<Avatar
src={user?.avatar}
userId={user?.id}
size="medium"
className={cn(
'-translate-y-1/2 -translate-x-1/2 ',
'border-2 border-transparent',
`${
me?.id === user?.id
? 'border-purple-500 rounded-full'
: isSelected
? 'border-blue-500 rounded-full'
: ''
}`
)}
/>
</a>
)
}
return (
<div
className={
'absolute border-2 border-transparent -translate-y-1/2 -translate-x-1/2'
}
style={style}
key={x.kind + x.position.x + x.position.y}
>
{/* @ts-ignore */}
{PointComponent[x.kind](x, isSelected, isAvailable, onClick)}
</div>
)
})

return (
<div className="relative">
<img
src={area.map}
alt={`${area.name} floor plan`}
className="block w-full opacity-60"
/>
{area.desks
.filter((x) => x.position)
.map((x) => {
const isSelected = selectedDeskId === x.id
const isAvailable = availableDeskIds.includes(x.id)
return (
<div
key={x.id}
className="absolute w-[1px] h-[1px]"
style={{ left: `${x.position?.x}%`, top: `${x.position?.y}%` }}
>
<Button
size="small"
kind={isSelected ? 'primary' : 'secondary'}
disabled={!isAvailable}
color={isSelected ? 'purple' : 'default'}
className={cn(
'2xl:hidden absolute -translate-y-2/4 -translate-x-2/4 whitespace-nowrap',
!isSelected && 'bg-gray-100'
)}
onClick={onClick(x.id)}
>
{x.name}
</Button>
<Button
kind={isSelected ? 'primary' : 'secondary'}
disabled={!isAvailable}
color={isSelected ? 'purple' : 'default'}
className={cn(
'hidden 2xl:inline absolute -translate-y-2/4 -translate-x-2/4 whitespace-nowrap',
!isSelected && 'bg-gray-100'
)}
onClick={onClick(x.id)}
>
{x.name}
</Button>
</div>
)
})}
<div className={cn(!!panZoom ? 'hidden' : 'block')}>
<img
src={area.map}
alt={`${area.name} floor plan`}
className="block w-full opacity-60"
/>
{mapObjects(1)}
</div>
<div
className={cn(
!!panZoom ? 'block' : 'hidden',
'border border-gray-300 rounded-sm'
)}
>
<ImageWithPanZoom
src={area.map}
alt={`${area.name} floor plan`}
className="block w-full opacity-60 object-contain overflow-hidden rounded-sm"
imageOverlayMappingFn={(scale: number) => mapObjects(scale)}
initialStartPosition={
initialStartPosition ? initialStartPosition.position : undefined
}
/>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion src/client/components/ui/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const Avatar: React.FC<Props> = ({
const [hasError, setHasError] = React.useState(false)
const setError = () => setHasError(true)
const resultClassName = cn(
'rounded-full bg-gray-200',
'block rounded-full bg-gray-200',
SIZE_CLASSNAME[size],
className
)
Expand Down
18 changes: 13 additions & 5 deletions src/client/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,13 @@ export const RoundButton = ({
icon,
className,
disabled = false,
size = 'small',
}: {
onClick: (ev: React.MouseEvent) => void
icon: Icon
className?: string
disabled?: boolean
size?: ButtonSize
}) => {
let IconComponent = Icons[icon]
if (!IconComponent) {
Expand All @@ -166,11 +168,13 @@ export const RoundButton = ({
}
return (
<Button
size="small"
size={size}
kind="secondary"
onClick={onClick}
className={cn(
'rounded-full h-9 w-9 flex justify-center items-center',
'rounded-full flex justify-center items-center',
size === 'small' && ' h-9 w-9',
size === 'normal' && ' h-14 w-14',
className
)}
disabled={disabled}
Expand Down Expand Up @@ -215,12 +219,16 @@ export const MoreButton: React.FC<MoreButtonProps> = ({
)
}

export const BackButton: React.FC<{ className?: string }> = ({ className }) => (
export const BackButton: React.FC<{
className?: string
onClick?: () => void
text?: string
}> = ({ className, onClick, text = 'Back' }) => (
<FButton
kind="link"
onClick={() => window.history.back()}
onClick={() => (!!onClick ? onClick() : window.history.back())}
className={cn('mb-4 ml-[-8px] text-text-tertiary', className)}
>
Back
{text}
</FButton>
)
Loading