Highlight availability segments and choose people manually to view

This commit is contained in:
Ben Grant 2021-05-05 21:37:35 +10:00
parent 0cfa931fe1
commit 01a8a26e04
9 changed files with 176 additions and 90 deletions

View file

@ -29,6 +29,7 @@ module.exports = async (req, res) => {
name: name,
created: currentTime,
times: event.times,
timezone: event.timezone,
},
};
@ -39,6 +40,7 @@ module.exports = async (req, res) => {
name: name,
created: currentTime,
times: event.times,
timezone: event.timezone,
});
} catch (e) {
console.error(e);

View file

@ -19,6 +19,8 @@ definitions:
type: "string"
name:
type: "string"
timezone:
type: "string"
created:
type: "integer"
times:
@ -81,6 +83,8 @@ paths:
properties:
name:
type: "string"
timezone:
type: "string"
times:
type: "array"
items:

View file

@ -1,10 +1,11 @@
import { useState, Fragment } from 'react';
import { useState, useEffect, Fragment } from 'react';
import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import { useSettingsStore } from 'stores';
import { Legend, Center } from 'components';
import {
Wrapper,
Container,
@ -21,6 +22,9 @@ import {
TimeLabels,
TimeLabel,
TimeSpace,
People,
Person,
StyledMain,
} from './availabilityViewerStyle';
dayjs.extend(localeData);
@ -38,83 +42,134 @@ const AvailabilityViewer = ({
}) => {
const [tooltip, setTooltip] = useState(null);
const timeFormat = useSettingsStore(state => state.timeFormat);
const [filteredPeople, setFilteredPeople] = useState([]);
const [touched, setTouched] = useState(false);
const [tempFocus, setTempFocus] = useState(null);
const [focusCount, setFocusCount] = useState(null);
useEffect(() => {
setFilteredPeople(people.map(p => p.name));
setTouched(people.length <= 1);
}, [people]);
return (
<Wrapper>
<Container>
<TimeLabels>
{!!timeLabels.length && timeLabels.map((label, i) =>
<TimeSpace key={i}>
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
</TimeSpace>
)}
</TimeLabels>
{dates.map((date, i) => {
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
return (
<Fragment key={i}>
<Date>
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
<>
<StyledMain>
<Legend
min={Math.min(min, filteredPeople.length)}
max={Math.min(max, filteredPeople.length)}
total={people.filter(p => p.availability.length > 0).length}
onSegmentFocus={count => setFocusCount(count)}
/>
<Center>Hover or tap the calendar below to see who is available</Center>
{!!people.length && (
<>
<Center>Click the names below to view people individually</Center>
<People>
{people.map((person, i) =>
<Person
key={i}
filtered={filteredPeople.includes(person.name)}
onClick={() => {
setTempFocus(null);
if (filteredPeople.includes(person.name)) {
if (!touched) {
setTouched(true);
setFilteredPeople([person.name]);
} else {
setFilteredPeople(filteredPeople.filter(n => n !== person.name));
}
} else {
setFilteredPeople([...filteredPeople, person.name]);
}
}}
onMouseOver={() => setTempFocus(person.name)}
onMouseOut={() => setTempFocus(null)}
>{person.name}</Person>
)}
</People>
</>
)}
</StyledMain>
<Times>
{timeLabels.map((timeLabel, i) => {
if (!timeLabel.time) return null;
if (!times.includes(`${timeLabel.time}-${date}`)) {
return (
<TimeSpace key={i} />
);
}
const time = `${timeLabel.time}-${date}`;
const peopleHere = people.filter(person => person.availability.includes(time)).map(person => person.name);
<Wrapper>
<Container>
<TimeLabels>
{!!timeLabels.length && timeLabels.map((label, i) =>
<TimeSpace key={i}>
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
</TimeSpace>
)}
</TimeLabels>
{dates.map((date, i) => {
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
return (
<Fragment key={i}>
<Date>
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
return (
<Time
key={i}
time={time}
className="time"
peopleCount={peopleHere.length}
aria-label={peopleHere.join(', ')}
maxPeople={max}
minPeople={min}
onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect();
const timeText = timeFormat === '12h' ? 'h:mma' : 'HH:mm';
setTooltip({
x: Math.round(cellBox.x + cellBox.width/2),
y: Math.round(cellBox.y + cellBox.height)+6,
available: `${peopleHere.length} / ${people.length} available`,
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
people: peopleHere.join(', '),
});
}}
onMouseLeave={() => {
setTooltip(null);
}}
/>
);
})}
</Times>
</Date>
{last && dates.length !== i+1 && (
<Spacer />
)}
</Fragment>
);
})}
</Container>
{tooltip && (
<Tooltip
x={tooltip.x}
y={tooltip.y}
>
<TooltipTitle>{tooltip.available}</TooltipTitle>
<TooltipDate>{tooltip.date}</TooltipDate>
<TooltipContent>{tooltip.people}</TooltipContent>
</Tooltip>
)}
</Wrapper>
<Times>
{timeLabels.map((timeLabel, i) => {
if (!timeLabel.time) return null;
if (!times.includes(`${timeLabel.time}-${date}`)) {
return (
<TimeSpace key={i} />
);
}
const time = `${timeLabel.time}-${date}`;
const peopleHere = tempFocus !== null
? people.filter(person => person.availability.includes(time) && tempFocus === person.name).map(person => person.name)
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name);
return (
<Time
key={i}
time={time}
className="time"
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
aria-label={peopleHere.join(', ')}
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect();
const timeText = timeFormat === '12h' ? 'h:mma' : 'HH:mm';
setTooltip({
x: Math.round(cellBox.x + cellBox.width/2),
y: Math.round(cellBox.y + cellBox.height)+6,
available: `${peopleHere.length} / ${people.length} available`,
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
people: peopleHere.join(', '),
});
}}
onMouseLeave={() => {
setTooltip(null);
}}
/>
);
})}
</Times>
</Date>
{last && dates.length !== i+1 && (
<Spacer />
)}
</Fragment>
);
})}
</Container>
{tooltip && (
<Tooltip
x={tooltip.x}
y={tooltip.y}
>
<TooltipTitle>{tooltip.available}</TooltipTitle>
<TooltipDate>{tooltip.date}</TooltipDate>
<TooltipContent>{tooltip.people}</TooltipContent>
</Tooltip>
)}
</Wrapper>
</>
);
};

View file

@ -127,3 +127,35 @@ export const TimeLabel = styled.label`
user-select: none;
width: 100%;
`;
export const StyledMain = styled.div`
width: 600px;
margin: 20px auto;
max-width: calc(100% - 60px);
`;
export const People = styled.div`
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: center;
margin: 14px auto;
`;
export const Person = styled.button`
font: inherit;
font-size: 15px;
border-radius: 3px;
border: 1px solid ${props => props.theme.text};
color: ${props => props.theme.text};
font-weight: 500;
background: transparent;
cursor: pointer;
padding: 2px 8px;
${props => props.filtered && `
background: ${props.theme.primary};
color: ${props.theme.background};
border-color: ${props.theme.primary};
`}
`;

View file

@ -11,6 +11,7 @@ const Legend = ({
min,
max,
total,
onSegmentFocus,
...props
}) => {
const theme = useTheme();
@ -19,9 +20,13 @@ const Legend = ({
<Wrapper>
<Label>{min}/{total} available</Label>
<Bar>
<Bar onMouseOut={() => onSegmentFocus(null)}>
{[...Array(max-min+1).keys()].map(i =>
<Grade key={i} color={`${theme.primary}${Math.round((i/(max-min))*255).toString(16)}`} />
<Grade
key={i}
color={`${theme.primary}${Math.round((i/(max-min))*255).toString(16)}`}
onMouseOver={() => onSegmentFocus(i+min)}
/>
)}
</Bar>

View file

@ -119,6 +119,7 @@ const Create = ({ offline }) => {
event: {
name: data.name,
times: times,
timezone: data.timezone,
},
});
setCreatedEvent(response.data);

View file

@ -13,7 +13,6 @@ import {
TextField,
SelectField,
Button,
Legend,
AvailabilityViewer,
AvailabilityEditor,
Error,
@ -398,14 +397,6 @@ const Event = (props) => {
{tab === 'group' ? (
<section id="group">
<StyledMain>
<Legend
min={min}
max={max}
total={people.filter(p => p.availability.length > 0).length}
/>
<Center>Hover or tap the calendar below to see who is available</Center>
</StyledMain>
<AvailabilityViewer
times={times}
timeLabels={timeLabels}

View file

@ -77,11 +77,6 @@ const Help = () => {
<P>Send the link to everyone you want to come.</P>
<P>After Jenny has sent the link to her friends and waited for them to also fill out their availabilities, she can now easily see them all on the heatmap below and choose the darkest area for a time that suits everyone!</P>
<P>In this example, 1pm to 3pm on Friday the 16th works for all Jenny's friends.</P>
<Legend
min={0}
max={5}
total={5}
/>
<AvailabilityViewer
times={["1100-12042021","1115-12042021","1130-12042021","1145-12042021","1200-12042021","1215-12042021","1230-12042021","1245-12042021","1300-12042021","1315-12042021","1330-12042021","1345-12042021","1400-12042021","1415-12042021","1430-12042021","1445-12042021","1500-12042021","1515-12042021","1530-12042021","1545-12042021","1600-12042021","1615-12042021","1630-12042021","1645-12042021","1100-13042021","1115-13042021","1130-13042021","1145-13042021","1200-13042021","1215-13042021","1230-13042021","1245-13042021","1300-13042021","1315-13042021","1330-13042021","1345-13042021","1400-13042021","1415-13042021","1430-13042021","1445-13042021","1500-13042021","1515-13042021","1530-13042021","1545-13042021","1600-13042021","1615-13042021","1630-13042021","1645-13042021","1100-14042021","1115-14042021","1130-14042021","1145-14042021","1200-14042021","1215-14042021","1230-14042021","1245-14042021","1300-14042021","1315-14042021","1330-14042021","1345-14042021","1400-14042021","1415-14042021","1430-14042021","1445-14042021","1500-14042021","1515-14042021","1530-14042021","1545-14042021","1600-14042021","1615-14042021","1630-14042021","1645-14042021","1100-15042021","1115-15042021","1130-15042021","1145-15042021","1200-15042021","1215-15042021","1230-15042021","1245-15042021","1300-15042021","1315-15042021","1330-15042021","1345-15042021","1400-15042021","1415-15042021","1430-15042021","1445-15042021","1500-15042021","1515-15042021","1530-15042021","1545-15042021","1600-15042021","1615-15042021","1630-15042021","1645-15042021","1100-16042021","1115-16042021","1130-16042021","1145-16042021","1200-16042021","1215-16042021","1230-16042021","1245-16042021","1300-16042021","1315-16042021","1330-16042021","1345-16042021","1400-16042021","1415-16042021","1430-16042021","1445-16042021","1500-16042021","1515-16042021","1530-16042021","1545-16042021","1600-16042021","1615-16042021","1630-16042021","1645-16042021"]}
timeLabels={[{"label":"11 AM","time":"1100"},{"label":"","time":"1115"},{"label":"","time":"1130"},{"label":"","time":"1145"},{"label":"12 PM","time":"1200"},{"label":"","time":"1215"},{"label":"","time":"1230"},{"label":"","time":"1245"},{"label":"1 PM","time":"1300"},{"label":"","time":"1315"},{"label":"","time":"1330"},{"label":"","time":"1345"},{"label":"2 PM","time":"1400"},{"label":"","time":"1415"},{"label":"","time":"1430"},{"label":"","time":"1445"},{"label":"3 PM","time":"1500"},{"label":"","time":"1515"},{"label":"","time":"1530"},{"label":"","time":"1545"},{"label":"4 PM","time":"1600"},{"label":"","time":"1615"},{"label":"","time":"1630"},{"label":"","time":"1645"},{"label":"5 PM","time":null}]}

View file

@ -132,6 +132,7 @@ const Home = ({ offline }) => {
event: {
name: data.name,
times: times,
timezone: data.timezone,
},
});
push(`/${response.data.id}`);