Highlight availability segments and choose people manually to view
This commit is contained in:
parent
0cfa931fe1
commit
01a8a26e04
|
|
@ -29,6 +29,7 @@ module.exports = async (req, res) => {
|
||||||
name: name,
|
name: name,
|
||||||
created: currentTime,
|
created: currentTime,
|
||||||
times: event.times,
|
times: event.times,
|
||||||
|
timezone: event.timezone,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ module.exports = async (req, res) => {
|
||||||
name: name,
|
name: name,
|
||||||
created: currentTime,
|
created: currentTime,
|
||||||
times: event.times,
|
times: event.times,
|
||||||
|
timezone: event.timezone,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: "string"
|
||||||
|
timezone:
|
||||||
|
type: "string"
|
||||||
created:
|
created:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
times:
|
times:
|
||||||
|
|
@ -81,6 +83,8 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: "string"
|
||||||
|
timezone:
|
||||||
|
type: "string"
|
||||||
times:
|
times:
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState, Fragment } from 'react';
|
import { useState, useEffect, Fragment } from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import localeData from 'dayjs/plugin/localeData';
|
import localeData from 'dayjs/plugin/localeData';
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
|
|
||||||
import { useSettingsStore } from 'stores';
|
import { useSettingsStore } from 'stores';
|
||||||
|
|
||||||
|
import { Legend, Center } from 'components';
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
Container,
|
Container,
|
||||||
|
|
@ -21,6 +22,9 @@ import {
|
||||||
TimeLabels,
|
TimeLabels,
|
||||||
TimeLabel,
|
TimeLabel,
|
||||||
TimeSpace,
|
TimeSpace,
|
||||||
|
People,
|
||||||
|
Person,
|
||||||
|
StyledMain,
|
||||||
} from './availabilityViewerStyle';
|
} from './availabilityViewerStyle';
|
||||||
|
|
||||||
dayjs.extend(localeData);
|
dayjs.extend(localeData);
|
||||||
|
|
@ -38,83 +42,134 @@ const AvailabilityViewer = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
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 (
|
return (
|
||||||
<Wrapper>
|
<>
|
||||||
<Container>
|
<StyledMain>
|
||||||
<TimeLabels>
|
<Legend
|
||||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
min={Math.min(min, filteredPeople.length)}
|
||||||
<TimeSpace key={i}>
|
max={Math.min(max, filteredPeople.length)}
|
||||||
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
total={people.filter(p => p.availability.length > 0).length}
|
||||||
</TimeSpace>
|
onSegmentFocus={count => setFocusCount(count)}
|
||||||
)}
|
/>
|
||||||
</TimeLabels>
|
<Center>Hover or tap the calendar below to see who is available</Center>
|
||||||
{dates.map((date, i) => {
|
{!!people.length && (
|
||||||
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;
|
<Center>Click the names below to view people individually</Center>
|
||||||
return (
|
<People>
|
||||||
<Fragment key={i}>
|
{people.map((person, i) =>
|
||||||
<Date>
|
<Person
|
||||||
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
|
key={i}
|
||||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
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>
|
<Wrapper>
|
||||||
{timeLabels.map((timeLabel, i) => {
|
<Container>
|
||||||
if (!timeLabel.time) return null;
|
<TimeLabels>
|
||||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||||
return (
|
<TimeSpace key={i}>
|
||||||
<TimeSpace key={i} />
|
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||||
);
|
</TimeSpace>
|
||||||
}
|
)}
|
||||||
const time = `${timeLabel.time}-${date}`;
|
</TimeLabels>
|
||||||
const peopleHere = people.filter(person => person.availability.includes(time)).map(person => person.name);
|
{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 (
|
<Times>
|
||||||
<Time
|
{timeLabels.map((timeLabel, i) => {
|
||||||
key={i}
|
if (!timeLabel.time) return null;
|
||||||
time={time}
|
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||||
className="time"
|
return (
|
||||||
peopleCount={peopleHere.length}
|
<TimeSpace key={i} />
|
||||||
aria-label={peopleHere.join(', ')}
|
);
|
||||||
maxPeople={max}
|
}
|
||||||
minPeople={min}
|
const time = `${timeLabel.time}-${date}`;
|
||||||
onMouseEnter={(e) => {
|
const peopleHere = tempFocus !== null
|
||||||
const cellBox = e.currentTarget.getBoundingClientRect();
|
? people.filter(person => person.availability.includes(time) && tempFocus === person.name).map(person => person.name)
|
||||||
const timeText = timeFormat === '12h' ? 'h:mma' : 'HH:mm';
|
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name);
|
||||||
setTooltip({
|
|
||||||
x: Math.round(cellBox.x + cellBox.width/2),
|
return (
|
||||||
y: Math.round(cellBox.y + cellBox.height)+6,
|
<Time
|
||||||
available: `${peopleHere.length} / ${people.length} available`,
|
key={i}
|
||||||
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
time={time}
|
||||||
people: peopleHere.join(', '),
|
className="time"
|
||||||
});
|
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
|
||||||
}}
|
aria-label={peopleHere.join(', ')}
|
||||||
onMouseLeave={() => {
|
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
||||||
setTooltip(null);
|
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({
|
||||||
</Times>
|
x: Math.round(cellBox.x + cellBox.width/2),
|
||||||
</Date>
|
y: Math.round(cellBox.y + cellBox.height)+6,
|
||||||
{last && dates.length !== i+1 && (
|
available: `${peopleHere.length} / ${people.length} available`,
|
||||||
<Spacer />
|
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
||||||
)}
|
people: peopleHere.join(', '),
|
||||||
</Fragment>
|
});
|
||||||
);
|
}}
|
||||||
})}
|
onMouseLeave={() => {
|
||||||
</Container>
|
setTooltip(null);
|
||||||
{tooltip && (
|
}}
|
||||||
<Tooltip
|
/>
|
||||||
x={tooltip.x}
|
);
|
||||||
y={tooltip.y}
|
})}
|
||||||
>
|
</Times>
|
||||||
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
</Date>
|
||||||
<TooltipDate>{tooltip.date}</TooltipDate>
|
{last && dates.length !== i+1 && (
|
||||||
<TooltipContent>{tooltip.people}</TooltipContent>
|
<Spacer />
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
</Fragment>
|
||||||
</Wrapper>
|
);
|
||||||
|
})}
|
||||||
|
</Container>
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip
|
||||||
|
x={tooltip.x}
|
||||||
|
y={tooltip.y}
|
||||||
|
>
|
||||||
|
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
||||||
|
<TooltipDate>{tooltip.date}</TooltipDate>
|
||||||
|
<TooltipContent>{tooltip.people}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,3 +127,35 @@ export const TimeLabel = styled.label`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
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};
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const Legend = ({
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
total,
|
total,
|
||||||
|
onSegmentFocus,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
@ -19,9 +20,13 @@ const Legend = ({
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Label>{min}/{total} available</Label>
|
<Label>{min}/{total} available</Label>
|
||||||
|
|
||||||
<Bar>
|
<Bar onMouseOut={() => onSegmentFocus(null)}>
|
||||||
{[...Array(max-min+1).keys()].map(i =>
|
{[...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>
|
</Bar>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ const Create = ({ offline }) => {
|
||||||
event: {
|
event: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
times: times,
|
times: times,
|
||||||
|
timezone: data.timezone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setCreatedEvent(response.data);
|
setCreatedEvent(response.data);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
SelectField,
|
SelectField,
|
||||||
Button,
|
Button,
|
||||||
Legend,
|
|
||||||
AvailabilityViewer,
|
AvailabilityViewer,
|
||||||
AvailabilityEditor,
|
AvailabilityEditor,
|
||||||
Error,
|
Error,
|
||||||
|
|
@ -398,14 +397,6 @@ const Event = (props) => {
|
||||||
|
|
||||||
{tab === 'group' ? (
|
{tab === 'group' ? (
|
||||||
<section id="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
|
<AvailabilityViewer
|
||||||
times={times}
|
times={times}
|
||||||
timeLabels={timeLabels}
|
timeLabels={timeLabels}
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,6 @@ const Help = () => {
|
||||||
<P>Send the link to everyone you want to come.</P>
|
<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>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>
|
<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
|
<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"]}
|
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}]}
|
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}]}
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ const Home = ({ offline }) => {
|
||||||
event: {
|
event: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
times: times,
|
times: times,
|
||||||
|
timezone: data.timezone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
push(`/${response.data.id}`);
|
push(`/${response.data.id}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue