Time zone support

This commit is contained in:
Ben Grant 2021-03-05 04:03:56 +11:00
parent ba1697ffc7
commit 76a36ed35f
16 changed files with 315 additions and 189 deletions

View file

@ -7,6 +7,7 @@ import {
Wrapper,
Container,
Date,
Times,
DateLabel,
DayLabel,
Spacer,
@ -20,8 +21,9 @@ dayjs.extend(localeData);
dayjs.extend(customParseFormat);
const AvailabilityEditor = ({
dates,
times,
timeLabels,
dates,
value = [],
onChange,
...props
@ -45,9 +47,9 @@ const AvailabilityEditor = ({
<Wrapper>
<Container>
<TimeLabels>
{!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
<TimeSpace key={i} time={time}>
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
{!!timeLabels.length && timeLabels.map((label, i) =>
<TimeSpace key={i}>
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
</TimeSpace>
)}
</TimeLabels>
@ -56,47 +58,59 @@ const AvailabilityEditor = ({
const last = dates.length === x+1 || dayjs(dates[x+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
return (
<Fragment key={x}>
<Date className={last ? 'last' : ''}>
<Date>
<DateLabel>{parsedDate.format('MMM D')}</DateLabel>
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
{times.map((time, y) =>
<Time
key={x+y}
time={time}
className="time"
selected={value.includes(`${time}-${date}`)}
selecting={selectingTimes.includes(`${time}-${date}`)}
mode={mode}
onPointerDown={(e) => {
e.preventDefault();
startPos.current = {x, y};
setMode(value.includes(`${time}-${date}`) ? 'remove' : 'add');
setSelectingTimes([`${time}-${date}`]);
e.currentTarget.releasePointerCapture(e.pointerId);
<Times>
{timeLabels.map((timeLabel, y) => {
if (!timeLabel.time) return null;
if (!times.includes(`${timeLabel.time}-${date}`)) {
return (
<TimeSpace key={x+y} />
);
}
const time = `${timeLabel.time}-${date}`;
document.addEventListener('pointerup', () => {
if (staticMode.current === 'add') {
onChange([...value, ...staticSelectingTimes.current]);
} else if (staticMode.current === 'remove') {
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
}
setMode(null);
}, { once: true });
}}
onPointerEnter={() => {
if (staticMode.current) {
let found = [];
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
found.push({y: cy, x: cx});
return (
<Time
key={x+y}
time={time}
className="time"
selected={value.includes(time)}
selecting={selectingTimes.includes(time)}
mode={mode}
onPointerDown={(e) => {
e.preventDefault();
startPos.current = {x, y};
setMode(value.includes(time) ? 'remove' : 'add');
setSelectingTimes([time]);
e.currentTarget.releasePointerCapture(e.pointerId);
document.addEventListener('pointerup', () => {
if (staticMode.current === 'add') {
onChange([...value, ...staticSelectingTimes.current]);
} else if (staticMode.current === 'remove') {
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
}
setMode(null);
}, { once: true });
}}
onPointerEnter={() => {
if (staticMode.current) {
let found = [];
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
found.push({y: cy, x: cx});
}
}
setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
}
}
setSelectingTimes(found.map(d => `${times[d.y]}-${dates[d.x]}`));
}
}}
/>
)}
}}
/>
);
})}
</Times>
</Date>
{last && dates.length !== x+1 && (
<Spacer />

View file

@ -2,20 +2,23 @@ import styled from '@emotion/styled';
export const Time = styled.div`
height: 10px;
border-left: 1px solid ${props => props.theme.primaryDark};
margin: 1px;
background-color: ${props => props.theme.background};
touch-action: none;
${props => props.time.slice(-2) === '00' && `
border-top: 1px solid ${props.theme.primaryDark};
${props => props.time.slice(2, 4) !== '00' && `
margin-top: -1px;
border-top: 2px solid transparent;
`}
${props => props.time.slice(-2) === '30' && `
border-top: 1px dotted ${props.theme.primaryDark};
${props => props.time.slice(2, 4) === '30' && `
margin-top: -1px;
border-top: 2px dotted ${props.theme.text};
`}
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
background-color: ${props.theme.primary};
`};
${props => props.mode === 'remove' && props.selecting && `
background-color: initial;
background-color: ${props.theme.background};
`};
`;

View file

@ -7,6 +7,7 @@ import {
Wrapper,
Container,
Date,
Times,
DateLabel,
DayLabel,
Time,
@ -24,8 +25,9 @@ dayjs.extend(localeData);
dayjs.extend(customParseFormat);
const AvailabilityViewer = ({
dates,
times,
timeLabels,
dates,
people = [],
min = 0,
max = 0,
@ -37,9 +39,9 @@ const AvailabilityViewer = ({
<Wrapper>
<Container>
<TimeLabels>
{!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
<TimeSpace key={i} time={time}>
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
{!!timeLabels.length && timeLabels.map((label, i) =>
<TimeSpace key={i}>
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
</TimeSpace>
)}
</TimeLabels>
@ -48,38 +50,47 @@ const AvailabilityViewer = ({
const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
return (
<Fragment key={i}>
<Date className={last ? 'last' : ''}>
<Date>
<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);
<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);
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();
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)).format('h:mma ddd, D MMM YYYY'),
people: peopleHere.join(', '),
});
}}
onMouseLeave={() => {
setTooltip(null);
}}
/>
);
})}
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();
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('h:mma ddd, D MMM YYYY'),
people: peopleHere.join(', '),
});
}}
onMouseLeave={() => {
setTooltip(null);
}}
/>
);
})}
</Times>
</Date>
{last && dates.length !== i+1 && (
<Spacer />

View file

@ -10,6 +10,7 @@ export const Container = styled.div`
box-sizing: border-box;
min-width: 100%;
align-items: flex-end;
justify-content: center;
padding: 0 calc(calc(100% - 600px) / 2);
@media (max-width: 660px) {
@ -24,13 +25,12 @@ export const Date = styled.div`
width: 60px;
min-width: 60px;
margin-bottom: 10px;
`;
& .time:last-of-type {
border-bottom: 1px solid ${props => props.theme.primaryDark};
}
&.last > .time {
border-right: 1px solid ${props => props.theme.primaryDark};
}
export const Times = styled.div`
display: flex;
flex-direction: column;
background-color: ${props => props.theme.text};
`;
export const DateLabel = styled.label`
@ -49,18 +49,22 @@ export const DayLabel = styled.label`
export const Time = styled.div`
height: 10px;
border-left: 1px solid ${props => props.theme.primaryDark};
margin: 1px;
background-color: ${props => props.theme.background};
${props => props.time.slice(-2) === '00' && `
border-top: 1px solid ${props.theme.primaryDark};
${props => props.time.slice(2, 4) !== '00' && `
margin-top: -1px;
border-top: 2px solid transparent;
`}
${props => props.time.slice(-2) === '30' && `
border-top: 1px dotted ${props.theme.primaryDark};
${props => props.time.slice(2, 4) === '30' && `
margin-top: -1px;
border-top: 2px dotted ${props.theme.text};
`}
background-color: ${props => `${props.theme.primary}${Math.round(((props.peopleCount-props.minPeople)/(props.maxPeople-props.minPeople))*255).toString(16)}`};
count: ${props => props.peopleCount};
max: ${props => props.maxPeople};
background-image: linear-gradient(
${props => `${props.theme.primary}${Math.round(((props.peopleCount-props.minPeople)/(props.maxPeople-props.minPeople))*255).toString(16)}`},
${props => `${props.theme.primary}${Math.round(((props.peopleCount-props.minPeople)/(props.maxPeople-props.minPeople))*255).toString(16)}`}
);
`;
export const Spacer = styled.div`
@ -110,13 +114,8 @@ export const TimeLabels = styled.div`
export const TimeSpace = styled.div`
height: 10px;
position: relative;
${props => props.time.slice(-2) === '00' && `
border-top: 1px solid transparent;
`}
${props => props.time.slice(-2) === '30' && `
border-top: 1px dotted transparent;
`}
border-top: 2px solid transparent;
background: ${props => props.theme.background};
`;
export const TimeLabel = styled.label`

View file

@ -82,7 +82,6 @@ export const Date = styled.div`
${props => props.isToday && `
font-weight: 900;
color: ${props.theme.primaryDark};
text-decoration: underline;
`}
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};

View file

@ -81,12 +81,13 @@ const TimeRangeField = ({
id={id}
type="hidden"
ref={register}
value={JSON.stringify(start > end ? {start: end, end: start} : {start, end})}
value={JSON.stringify({start, end})}
{...props}
/>
<Range ref={rangeRef}>
<Selected start={start > end ? end : start} end={start > end ? start : end} />
<Selected start={start} end={start > end ? 24 : end} />
{start > end && <Selected start={start > end ? 0 : start} end={end} />}
<Handle
value={start}
label={times[start]}

View file

@ -70,4 +70,5 @@ export const Selected = styled.div`
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
top: 0;
background-color: ${props => props.theme.primary};
border-radius: 2px;
`;