Event page and availability viewer

This commit is contained in:
Ben Grant 2021-03-03 04:01:32 +11:00
parent baac453964
commit 3b67241107
15 changed files with 578 additions and 38 deletions

View file

@ -0,0 +1,91 @@
import { useState } from 'react';
import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import {
Wrapper,
Container,
Date,
DateLabel,
DayLabel,
Time,
Spacer,
Tooltip,
TooltipTitle,
TooltipDate,
TooltipContent,
} from './availabilityViewerStyle';
dayjs.extend(localeData);
dayjs.extend(customParseFormat);
const AvailabilityViewer = ({
dates,
times,
people = [],
...props
}) => {
const [tooltip, setTooltip] = useState(null);
return (
<Wrapper>
<Container>
{dates.map((date, i) => {
const parsedDate = dayjs(date, 'DDMMYYYY');
const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
return (
<>
<Date key={i} className={last ? 'last' : ''}>
<DateLabel>{parsedDate.format('MMM D')}</DateLabel>
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
{times.map((time, i) => {
const peopleHere = people.filter(person => person.availability.includes(`${time}-${date}`)).map(person => person.name);
return (
<Time
key={i}
time={time}
className="time"
people={peopleHere}
aria-label={peopleHere.join(', ')}
totalPeople={people.length}
onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect();
setTooltip({
x: Math.round(cellBox.x + cellBox.width),
y: Math.round(cellBox.y + cellBox.height),
available: `${peopleHere.length} / ${people.length} available`,
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(-2)).format('h:mma ddd, D MMM YYYY'),
people: peopleHere.join(', '),
});
}}
onMouseLeave={() => {
setTooltip(null);
}}
/>
);
})}
</Date>
{last && dates.length !== i+1 && (
<Spacer />
)}
</>
);
})}
</Container>
{tooltip && (
<Tooltip
x={tooltip.x}
y={tooltip.y}
>
<TooltipTitle>{tooltip.available}</TooltipTitle>
<TooltipDate>{tooltip.date}</TooltipDate>
<TooltipContent>{tooltip.people}</TooltipContent>
</Tooltip>
)}
</Wrapper>
);
};
export default AvailabilityViewer;

View file

@ -0,0 +1,91 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
overflow-x: auto;
margin: 20px 0;
`;
export const Container = styled.div`
display: inline-flex;
box-sizing: border-box;
min-width: 100%;
align-items: flex-start;
padding: 0 calc(calc(100% - 600px) / 2);
`;
export const Date = styled.div`
flex-shrink: 0;
display: flex;
flex-direction: column;
width: 60px;
min-width: 60px;
& .time:last-of-type {
border-bottom: 1px solid ${props => props.theme.primaryDark};
}
&.last > .time {
border-right: 1px solid ${props => props.theme.primaryDark};
}
`;
export const DateLabel = styled.label`
display: block;
font-size: 12px;
text-align: center;
user-select: none;
`;
export const DayLabel = styled.label`
display: block;
font-size: 15px;
text-align: center;
user-select: none;
`;
export const Time = styled.div`
height: 10px;
border-left: 1px solid ${props => props.theme.primaryDark};
${props => props.time.slice(-2) === '00' && `
border-top: 1px solid ${props.theme.primaryDark};
`}
${props => props.time.slice(-2) === '30' && `
border-top: 1px dotted ${props.theme.primaryDark};
`}
background-color: ${props => `${props.theme.primary}${Math.round((props.people.length/(props.totalPeople))*255).toString(16)}`};
`;
export const Spacer = styled.div`
width: 12px;
flex-shrink: 0;
`;
export const Tooltip = styled.div`
position: fixed;
top: ${props => props.y+6}px;
left: ${props => props.x+6}px;
border: 1px solid ${props => props.theme.text};
border-radius: 3px;
padding: 4px 8px;
background-color: ${props => props.theme.background};
max-width: 200px;
`;
export const TooltipTitle = styled.span`
font-size: 15px;
display: block;
font-weight: 700;
`;
export const TooltipDate = styled.span`
font-size: 13px;
display: block;
opacity: .7;
font-weight: 700;
`;
export const TooltipContent = styled.span`
font-size: 13px;
display: block;
`;

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import dayjs from 'dayjs';
import isToday from 'dayjs/plugin/isToday';
import localeData from 'dayjs/plugin/localeData';
import { Button } from 'components';
import {
@ -15,31 +16,7 @@ import {
} from './calendarFieldStyle';
dayjs.extend(isToday);
const days = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
dayjs.extend(localeData);
const calculateMonth = (month, year) => {
const date = dayjs().month(month).year(year);
@ -123,7 +100,7 @@ const CalendarField = ({
}
}}
>&lt;</Button>
<span>{months[month]} {year}</span>
<span>{dayjs.months()[month]} {year}</span>
<Button
buttonHeight="30px"
buttonWidth="30px"
@ -141,7 +118,7 @@ const CalendarField = ({
</CalendarHeader>
<CalendarDays>
{days.map((name, i) =>
{dayjs.weekdaysShort().map((name, i) =>
<Day key={i}>{name}</Day>
)}
</CalendarDays>
@ -152,7 +129,7 @@ const CalendarField = ({
key={y+x}
otherMonth={date.month() !== month}
isToday={date.isToday()}
title={`${date.date()} ${months[date.month()]}${date.isToday() ? ' (today)' : ''}`}
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ' (today)' : ''}`}
selected={selectedDates.includes(date.format('DDMMYYYY'))}
selecting={selectingDates.includes(date)}
mode={mode}

View file

@ -1,7 +1,9 @@
import styled from '@emotion/styled';
const Center = styled.div`
text-align: center;
display: flex;
align-items: center;
justify-content: center;
`;
export default Center;

View file

@ -0,0 +1,32 @@
import { useTheme } from '@emotion/react';
import {
Wrapper,
Label,
Bar,
Grade,
} from './legendStyle';
const Legend = ({
min,
max,
...props
}) => {
const theme = useTheme();
return (
<Wrapper>
<Label>{min}/{max} available</Label>
<Bar>
{[...Array(max-min+1).keys()].map(i =>
<Grade key={i} color={`${theme.primary}${Math.round((i/(max-min))*255).toString(16)}`} />
)}
</Bar>
<Label>{max}/{max} available</Label>
</Wrapper>
);
};
export default Legend;

View file

@ -0,0 +1,42 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
margin: 10px 0;
display: flex;
align-items: center;
justify-content: center;
& label:last-of-type {
text-align: right;
}
@media (max-width: 400px) {
display: block;
}
`;
export const Label = styled.label`
display: block;
font-size: 14px;
text-align: left;
`;
export const Bar = styled.div`
display: flex;
width: 40%;
height: 20px;
border-radius: 3px;
overflow: hidden;
margin: 0 8px;
border: 1px solid ${props => props.theme.primaryLight};
@media (max-width: 400px) {
width: 100%;
margin: 8px 0;
}
`;
export const Grade = styled.div`
flex: 1;
background-color: ${props => props.color};
`;

View file

@ -10,11 +10,12 @@ const SelectField = ({
subLabel,
id,
options = [],
inline = false,
register,
...props
}) => (
<Wrapper>
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
<Wrapper inline={inline}>
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
<StyledSelect

View file

@ -2,12 +2,20 @@ import styled from '@emotion/styled';
export const Wrapper = styled.div`
margin: 30px 0;
${props => props.inline && `
margin: 0;
`}
`;
export const StyledLabel = styled.label`
display: block;
padding-bottom: 4px;
font-size: 18px;
${props => props.inline && `
font-size: 16px;
`}
`;
export const StyledSubLabel = styled.label`

View file

@ -9,11 +9,12 @@ const TextField = ({
label,
subLabel,
id,
inline = false,
register,
...props
}) => (
<Wrapper>
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
<Wrapper inline={inline}>
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
<StyledInput id={id} ref={register} {...props} />
</Wrapper>

View file

@ -2,12 +2,20 @@ import styled from '@emotion/styled';
export const Wrapper = styled.div`
margin: 30px 0;
${props => props.inline && `
margin: 0;
`}
`;
export const StyledLabel = styled.label`
display: block;
padding-bottom: 4px;
font-size: 18px;
${props => props.inline && `
font-size: 16px;
`}
`;
export const StyledSubLabel = styled.label`

View file

@ -4,6 +4,8 @@ export { default as CalendarField } from './CalendarField/CalendarField';
export { default as TimeRangeField } from './TimeRangeField/TimeRangeField';
export { default as Button } from './Button/Button';
export { default as Legend } from './Legend/Legend';
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer';
export { default as Center } from './Center/Center';
export { default as Donate } from './Donate/Donate';