Time zone support
This commit is contained in:
parent
ba1697ffc7
commit
76a36ed35f
|
|
@ -3,7 +3,7 @@ const dayjs = require('dayjs');
|
||||||
const adjectives = require('../res/adjectives.json');
|
const adjectives = require('../res/adjectives.json');
|
||||||
const crabs = require('../res/crabs.json');
|
const crabs = require('../res/crabs.json');
|
||||||
|
|
||||||
String.prototype.capitalize = () => this.charAt(0).toUpperCase() + this.slice(1);
|
const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
|
||||||
const generateId = (name) => {
|
const generateId = (name) => {
|
||||||
const id = name.trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-');
|
const id = name.trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-');
|
||||||
|
|
@ -12,7 +12,7 @@ const generateId = (name) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateName = () => {
|
const generateName = () => {
|
||||||
return `${adjectives[Math.floor(Math.random() * adjectives.length)].capitalize()} ${crabs[Math.floor(Math.random() * crabs.length)]} Crab`;
|
return `${capitalize(adjectives[Math.floor(Math.random() * adjectives.length)])} ${crabs[Math.floor(Math.random() * crabs.length)]} Crab`;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = async (req, res) => {
|
module.exports = async (req, res) => {
|
||||||
|
|
@ -28,9 +28,7 @@ module.exports = async (req, res) => {
|
||||||
data: {
|
data: {
|
||||||
name: name,
|
name: name,
|
||||||
created: currentTime,
|
created: currentTime,
|
||||||
startTime: event.startTime,
|
times: event.times,
|
||||||
endTime: event.endTime,
|
|
||||||
dates: event.dates,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -40,9 +38,7 @@ module.exports = async (req, res) => {
|
||||||
id: eventId,
|
id: eventId,
|
||||||
name: name,
|
name: name,
|
||||||
created: currentTime,
|
created: currentTime,
|
||||||
startTime: event.startTime,
|
times: event.times,
|
||||||
endTime: event.endTime,
|
|
||||||
dates: event.dates,
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ module.exports = async (req, res) => {
|
||||||
let personCount = null;
|
let personCount = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query = req.datastore.createQuery(['__Stat_Kind__']);
|
const eventQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', 'Event');
|
||||||
|
const personQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', 'Person');
|
||||||
|
|
||||||
eventCount = (await req.datastore.runQuery(query.filter('kind_name', 'Event')))[0][0].count;
|
eventCount = (await req.datastore.runQuery(eventQuery))[0][0].count;
|
||||||
personCount = (await req.datastore.runQuery(query.filter('kind_name', 'Person')))[0][0].count;
|
personCount = (await req.datastore.runQuery(personQuery))[0][0].count;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,7 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
created:
|
created:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
startTime:
|
times:
|
||||||
type: "string"
|
|
||||||
endTime:
|
|
||||||
type: "string"
|
|
||||||
dates:
|
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
type: "string"
|
type: "string"
|
||||||
|
|
@ -82,11 +78,7 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: "string"
|
type: "string"
|
||||||
startTime:
|
times:
|
||||||
type: "integer"
|
|
||||||
endTime:
|
|
||||||
type: "integer"
|
|
||||||
dates:
|
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
type: "string"
|
type: "string"
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
Container,
|
Container,
|
||||||
Date,
|
Date,
|
||||||
|
Times,
|
||||||
DateLabel,
|
DateLabel,
|
||||||
DayLabel,
|
DayLabel,
|
||||||
Spacer,
|
Spacer,
|
||||||
|
|
@ -20,8 +21,9 @@ dayjs.extend(localeData);
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const AvailabilityEditor = ({
|
const AvailabilityEditor = ({
|
||||||
dates,
|
|
||||||
times,
|
times,
|
||||||
|
timeLabels,
|
||||||
|
dates,
|
||||||
value = [],
|
value = [],
|
||||||
onChange,
|
onChange,
|
||||||
...props
|
...props
|
||||||
|
|
@ -45,9 +47,9 @@ const AvailabilityEditor = ({
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Container>
|
<Container>
|
||||||
<TimeLabels>
|
<TimeLabels>
|
||||||
{!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
|
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||||
<TimeSpace key={i} time={time}>
|
<TimeSpace key={i}>
|
||||||
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
|
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||||
</TimeSpace>
|
</TimeSpace>
|
||||||
)}
|
)}
|
||||||
</TimeLabels>
|
</TimeLabels>
|
||||||
|
|
@ -56,47 +58,59 @@ const AvailabilityEditor = ({
|
||||||
const last = dates.length === x+1 || dayjs(dates[x+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
|
const last = dates.length === x+1 || dayjs(dates[x+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
|
||||||
return (
|
return (
|
||||||
<Fragment key={x}>
|
<Fragment key={x}>
|
||||||
<Date className={last ? 'last' : ''}>
|
<Date>
|
||||||
<DateLabel>{parsedDate.format('MMM D')}</DateLabel>
|
<DateLabel>{parsedDate.format('MMM D')}</DateLabel>
|
||||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||||
|
|
||||||
{times.map((time, y) =>
|
<Times>
|
||||||
<Time
|
{timeLabels.map((timeLabel, y) => {
|
||||||
key={x+y}
|
if (!timeLabel.time) return null;
|
||||||
time={time}
|
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||||
className="time"
|
return (
|
||||||
selected={value.includes(`${time}-${date}`)}
|
<TimeSpace key={x+y} />
|
||||||
selecting={selectingTimes.includes(`${time}-${date}`)}
|
);
|
||||||
mode={mode}
|
}
|
||||||
onPointerDown={(e) => {
|
const time = `${timeLabel.time}-${date}`;
|
||||||
e.preventDefault();
|
|
||||||
startPos.current = {x, y};
|
|
||||||
setMode(value.includes(`${time}-${date}`) ? 'remove' : 'add');
|
|
||||||
setSelectingTimes([`${time}-${date}`]);
|
|
||||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
||||||
|
|
||||||
document.addEventListener('pointerup', () => {
|
return (
|
||||||
if (staticMode.current === 'add') {
|
<Time
|
||||||
onChange([...value, ...staticSelectingTimes.current]);
|
key={x+y}
|
||||||
} else if (staticMode.current === 'remove') {
|
time={time}
|
||||||
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
|
className="time"
|
||||||
}
|
selected={value.includes(time)}
|
||||||
setMode(null);
|
selecting={selectingTimes.includes(time)}
|
||||||
}, { once: true });
|
mode={mode}
|
||||||
}}
|
onPointerDown={(e) => {
|
||||||
onPointerEnter={() => {
|
e.preventDefault();
|
||||||
if (staticMode.current) {
|
startPos.current = {x, y};
|
||||||
let found = [];
|
setMode(value.includes(time) ? 'remove' : 'add');
|
||||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
setSelectingTimes([time]);
|
||||||
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||||
found.push({y: cy, x: cx});
|
|
||||||
|
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>
|
</Date>
|
||||||
{last && dates.length !== x+1 && (
|
{last && dates.length !== x+1 && (
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,23 @@ import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Time = styled.div`
|
export const Time = styled.div`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-left: 1px solid ${props => props.theme.primaryDark};
|
margin: 1px;
|
||||||
|
background-color: ${props => props.theme.background};
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
|
||||||
${props => props.time.slice(-2) === '00' && `
|
${props => props.time.slice(2, 4) !== '00' && `
|
||||||
border-top: 1px solid ${props.theme.primaryDark};
|
margin-top: -1px;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(-2) === '30' && `
|
${props => props.time.slice(2, 4) === '30' && `
|
||||||
border-top: 1px dotted ${props.theme.primaryDark};
|
margin-top: -1px;
|
||||||
|
border-top: 2px dotted ${props.theme.text};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||||
background-color: ${props.theme.primary};
|
background-color: ${props.theme.primary};
|
||||||
`};
|
`};
|
||||||
${props => props.mode === 'remove' && props.selecting && `
|
${props => props.mode === 'remove' && props.selecting && `
|
||||||
background-color: initial;
|
background-color: ${props.theme.background};
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
Container,
|
Container,
|
||||||
Date,
|
Date,
|
||||||
|
Times,
|
||||||
DateLabel,
|
DateLabel,
|
||||||
DayLabel,
|
DayLabel,
|
||||||
Time,
|
Time,
|
||||||
|
|
@ -24,8 +25,9 @@ dayjs.extend(localeData);
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const AvailabilityViewer = ({
|
const AvailabilityViewer = ({
|
||||||
dates,
|
|
||||||
times,
|
times,
|
||||||
|
timeLabels,
|
||||||
|
dates,
|
||||||
people = [],
|
people = [],
|
||||||
min = 0,
|
min = 0,
|
||||||
max = 0,
|
max = 0,
|
||||||
|
|
@ -37,9 +39,9 @@ const AvailabilityViewer = ({
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Container>
|
<Container>
|
||||||
<TimeLabels>
|
<TimeLabels>
|
||||||
{!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
|
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||||
<TimeSpace key={i} time={time}>
|
<TimeSpace key={i}>
|
||||||
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
|
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||||
</TimeSpace>
|
</TimeSpace>
|
||||||
)}
|
)}
|
||||||
</TimeLabels>
|
</TimeLabels>
|
||||||
|
|
@ -48,38 +50,47 @@ const AvailabilityViewer = ({
|
||||||
const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
|
const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
|
||||||
return (
|
return (
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
<Date className={last ? 'last' : ''}>
|
<Date>
|
||||||
<DateLabel>{parsedDate.format('MMM D')}</DateLabel>
|
<DateLabel>{parsedDate.format('MMM D')}</DateLabel>
|
||||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||||
|
|
||||||
{times.map((time, i) => {
|
<Times>
|
||||||
const peopleHere = people.filter(person => person.availability.includes(`${time}-${date}`)).map(person => person.name);
|
{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 (
|
return (
|
||||||
<Time
|
<Time
|
||||||
key={i}
|
key={i}
|
||||||
time={time}
|
time={time}
|
||||||
className="time"
|
className="time"
|
||||||
peopleCount={peopleHere.length}
|
peopleCount={peopleHere.length}
|
||||||
aria-label={peopleHere.join(', ')}
|
aria-label={peopleHere.join(', ')}
|
||||||
maxPeople={max}
|
maxPeople={max}
|
||||||
minPeople={min}
|
minPeople={min}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
const cellBox = e.currentTarget.getBoundingClientRect();
|
const cellBox = e.currentTarget.getBoundingClientRect();
|
||||||
setTooltip({
|
setTooltip({
|
||||||
x: Math.round(cellBox.x + cellBox.width/2),
|
x: Math.round(cellBox.x + cellBox.width/2),
|
||||||
y: Math.round(cellBox.y + cellBox.height)+6,
|
y: Math.round(cellBox.y + cellBox.height)+6,
|
||||||
available: `${peopleHere.length} / ${people.length} available`,
|
available: `${peopleHere.length} / ${people.length} available`,
|
||||||
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(-2)).format('h:mma ddd, D MMM YYYY'),
|
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format('h:mma ddd, D MMM YYYY'),
|
||||||
people: peopleHere.join(', '),
|
people: peopleHere.join(', '),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setTooltip(null);
|
setTooltip(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Times>
|
||||||
</Date>
|
</Date>
|
||||||
{last && dates.length !== i+1 && (
|
{last && dates.length !== i+1 && (
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export const Container = styled.div`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
padding: 0 calc(calc(100% - 600px) / 2);
|
padding: 0 calc(calc(100% - 600px) / 2);
|
||||||
|
|
||||||
@media (max-width: 660px) {
|
@media (max-width: 660px) {
|
||||||
|
|
@ -24,13 +25,12 @@ export const Date = styled.div`
|
||||||
width: 60px;
|
width: 60px;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
& .time:last-of-type {
|
export const Times = styled.div`
|
||||||
border-bottom: 1px solid ${props => props.theme.primaryDark};
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
&.last > .time {
|
background-color: ${props => props.theme.text};
|
||||||
border-right: 1px solid ${props => props.theme.primaryDark};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DateLabel = styled.label`
|
export const DateLabel = styled.label`
|
||||||
|
|
@ -49,18 +49,22 @@ export const DayLabel = styled.label`
|
||||||
|
|
||||||
export const Time = styled.div`
|
export const Time = styled.div`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-left: 1px solid ${props => props.theme.primaryDark};
|
margin: 1px;
|
||||||
|
background-color: ${props => props.theme.background};
|
||||||
|
|
||||||
${props => props.time.slice(-2) === '00' && `
|
${props => props.time.slice(2, 4) !== '00' && `
|
||||||
border-top: 1px solid ${props.theme.primaryDark};
|
margin-top: -1px;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(-2) === '30' && `
|
${props => props.time.slice(2, 4) === '30' && `
|
||||||
border-top: 1px dotted ${props.theme.primaryDark};
|
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)}`};
|
background-image: linear-gradient(
|
||||||
count: ${props => props.peopleCount};
|
${props => `${props.theme.primary}${Math.round(((props.peopleCount-props.minPeople)/(props.maxPeople-props.minPeople))*255).toString(16)}`},
|
||||||
max: ${props => props.maxPeople};
|
${props => `${props.theme.primary}${Math.round(((props.peopleCount-props.minPeople)/(props.maxPeople-props.minPeople))*255).toString(16)}`}
|
||||||
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Spacer = styled.div`
|
export const Spacer = styled.div`
|
||||||
|
|
@ -110,13 +114,8 @@ export const TimeLabels = styled.div`
|
||||||
export const TimeSpace = styled.div`
|
export const TimeSpace = styled.div`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
${props => props.time.slice(-2) === '00' && `
|
background: ${props => props.theme.background};
|
||||||
border-top: 1px solid transparent;
|
|
||||||
`}
|
|
||||||
${props => props.time.slice(-2) === '30' && `
|
|
||||||
border-top: 1px dotted transparent;
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TimeLabel = styled.label`
|
export const TimeLabel = styled.label`
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ export const Date = styled.div`
|
||||||
${props => props.isToday && `
|
${props => props.isToday && `
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: ${props.theme.primaryDark};
|
color: ${props.theme.primaryDark};
|
||||||
text-decoration: underline;
|
|
||||||
`}
|
`}
|
||||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||||
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,13 @@ const TimeRangeField = ({
|
||||||
id={id}
|
id={id}
|
||||||
type="hidden"
|
type="hidden"
|
||||||
ref={register}
|
ref={register}
|
||||||
value={JSON.stringify(start > end ? {start: end, end: start} : {start, end})}
|
value={JSON.stringify({start, end})}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Range ref={rangeRef}>
|
<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
|
<Handle
|
||||||
value={start}
|
value={start}
|
||||||
label={times[start]}
|
label={times[start]}
|
||||||
|
|
|
||||||
|
|
@ -70,4 +70,5 @@ export const Selected = styled.div`
|
||||||
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: ${props => props.theme.primary};
|
||||||
|
border-radius: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Center,
|
Center,
|
||||||
|
|
@ -33,6 +38,10 @@ import api from 'services';
|
||||||
import logo from 'res/logo.svg';
|
import logo from 'res/logo.svg';
|
||||||
import timezones from 'res/timezones.json';
|
import timezones from 'res/timezones.json';
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const Event = (props) => {
|
const Event = (props) => {
|
||||||
const { register, handleSubmit } = useForm();
|
const { register, handleSubmit } = useForm();
|
||||||
const { id } = props.match.params;
|
const { id } = props.match.params;
|
||||||
|
|
@ -46,37 +55,19 @@ const Event = (props) => {
|
||||||
const [event, setEvent] = useState(null);
|
const [event, setEvent] = useState(null);
|
||||||
const [people, setPeople] = useState([]);
|
const [people, setPeople] = useState([]);
|
||||||
|
|
||||||
|
const [times, setTimes] = useState([]);
|
||||||
|
const [timeLabels, setTimeLabels] = useState([]);
|
||||||
|
const [dates, setDates] = useState([]);
|
||||||
const [min, setMin] = useState(0);
|
const [min, setMin] = useState(0);
|
||||||
const [max, setMax] = useState(0);
|
const [max, setMax] = useState(0);
|
||||||
|
|
||||||
const fetchPeople = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.get(`/event/${id}/people`);
|
|
||||||
setPeople(response.data.people);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchEvent = async () => {
|
const fetchEvent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/event/${id}`);
|
const response = await api.get(`/event/${id}`);
|
||||||
let times = [];
|
|
||||||
for (let i = response.data.startTime; i < response.data.endTime; i++) {
|
|
||||||
let hour = `${i}`.padStart(2, '0');
|
|
||||||
times.push(
|
|
||||||
`${hour}00`,
|
|
||||||
`${hour}15`,
|
|
||||||
`${hour}30`,
|
|
||||||
`${hour}45`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvent({
|
setEvent(response.data);
|
||||||
...response.data,
|
document.title = `${response.data.name} | Crab Fit`;
|
||||||
times,
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -85,28 +76,49 @@ const Event = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchEvent();
|
fetchEvent();
|
||||||
fetchPeople();
|
}, [id]);
|
||||||
}, [fetchPeople, id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const fetchPeople = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/event/${id}/people`);
|
||||||
|
const adjustedPeople = response.data.people.map(person => ({
|
||||||
|
...person,
|
||||||
|
availability: person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')),
|
||||||
|
}));
|
||||||
|
setPeople(adjustedPeople);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tab === 'group') {
|
if (tab === 'group') {
|
||||||
fetchPeople();
|
fetchPeople();
|
||||||
}
|
}
|
||||||
}, [fetchPeople, tab]);
|
}, [tab, id, timezone]);
|
||||||
|
|
||||||
|
// Convert to timezone and expand minute segments
|
||||||
|
useEffect(() => {
|
||||||
|
if (event) {
|
||||||
|
setTimes(event.times.reduce(
|
||||||
|
(allTimes, time) => {
|
||||||
|
const date = dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone);
|
||||||
|
return [
|
||||||
|
...allTimes,
|
||||||
|
date.minute(0).format('HHmm-DDMMYYYY'),
|
||||||
|
date.minute(15).format('HHmm-DDMMYYYY'),
|
||||||
|
date.minute(30).format('HHmm-DDMMYYYY'),
|
||||||
|
date.minute(45).format('HHmm-DDMMYYYY'),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
).sort((a, b) => dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'))));
|
||||||
|
}
|
||||||
|
}, [event, timezone]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (event && !!people.length) {
|
if (!!times.length && !!people.length) {
|
||||||
const datetimes = event.dates.reduce(
|
setMin(times.reduce((min, time) => {
|
||||||
(dates, date) => {
|
|
||||||
let times = [];
|
|
||||||
event.times.forEach(time => {
|
|
||||||
times.push(`${time}-${date}`);
|
|
||||||
});
|
|
||||||
return [...dates, ...times];
|
|
||||||
}
|
|
||||||
, []
|
|
||||||
);
|
|
||||||
setMin(datetimes.reduce((min, time) => {
|
|
||||||
let total = people.reduce(
|
let total = people.reduce(
|
||||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||||
0
|
0
|
||||||
|
|
@ -115,7 +127,7 @@ const Event = (props) => {
|
||||||
},
|
},
|
||||||
Infinity
|
Infinity
|
||||||
));
|
));
|
||||||
setMax(datetimes.reduce((max, time) => {
|
setMax(times.reduce((max, time) => {
|
||||||
let total = people.reduce(
|
let total = people.reduce(
|
||||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||||
0
|
0
|
||||||
|
|
@ -125,7 +137,63 @@ const Event = (props) => {
|
||||||
-Infinity
|
-Infinity
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}, [event, people]);
|
}, [times, people]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!times.length) {
|
||||||
|
setTimeLabels(times.reduce((labels, datetime) => {
|
||||||
|
const time = datetime.substring(0, 4);
|
||||||
|
if (labels.includes(time)) return labels;
|
||||||
|
return [...labels, time];
|
||||||
|
}, [])
|
||||||
|
.sort((a, b) => parseInt(a) - parseInt(b))
|
||||||
|
.reduce((labels, time, i, allTimes) => {
|
||||||
|
if (time.substring(2) === '30') return [...labels, { label: '', time }];
|
||||||
|
if (allTimes.length - 1 === i) return [
|
||||||
|
...labels,
|
||||||
|
{ label: '', time },
|
||||||
|
{ label: dayjs(time, 'HHmm').add(1, 'hour').format('h A'), time: null }
|
||||||
|
];
|
||||||
|
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
|
||||||
|
...labels,
|
||||||
|
{ label: '', time },
|
||||||
|
{ label: dayjs(time, 'HHmm').add(1, 'hour').format('h A'), time: 'space' },
|
||||||
|
{ label: '', time: 'space' },
|
||||||
|
{ label: '', time: 'space' },
|
||||||
|
];
|
||||||
|
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
|
||||||
|
return [...labels, { label: dayjs(time, 'HHmm').format('h A'), time }];
|
||||||
|
}, []));
|
||||||
|
|
||||||
|
setDates(times.reduce((allDates, time) => {
|
||||||
|
if (time.substring(2, 4) !== '00') return allDates;
|
||||||
|
const date = time.substring(5);
|
||||||
|
if (allDates.includes(date)) return allDates;
|
||||||
|
return [...allDates, date];
|
||||||
|
}, []));
|
||||||
|
}
|
||||||
|
}, [times]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
|
||||||
|
const adjustedUser = {
|
||||||
|
...response.data,
|
||||||
|
availability: response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')),
|
||||||
|
};
|
||||||
|
setUser(adjustedUser);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
console.log('FETCHING', timezone);
|
||||||
|
fetchUser();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [timezone]);
|
||||||
|
|
||||||
const onSubmit = async data => {
|
const onSubmit = async data => {
|
||||||
setIsLoginLoading(true);
|
setIsLoginLoading(true);
|
||||||
|
|
@ -137,7 +205,11 @@ const Event = (props) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setPassword(data.password);
|
setPassword(data.password);
|
||||||
setUser(response.data);
|
const adjustedUser = {
|
||||||
|
...response.data,
|
||||||
|
availability: response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY')),
|
||||||
|
};
|
||||||
|
setUser(adjustedUser);
|
||||||
setTab('you');
|
setTab('you');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) {
|
if (e.status === 401) {
|
||||||
|
|
@ -178,10 +250,10 @@ const Event = (props) => {
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||||
<ShareInfo isLoading={isLoading}>{!!event?.name && `https://crab.fit/${id}`}</ShareInfo>
|
<ShareInfo>https://crab.fit/{id}</ShareInfo>
|
||||||
<ShareInfo isLoading={isLoading}>
|
<ShareInfo isLoading={isLoading}>
|
||||||
{!!event?.name &&
|
{!!event?.name &&
|
||||||
<>Copy the link to this page, or share via <a href={`mailto:?subject=${encodeURIComponent(`Scheduling ${event?.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${id}`)}`}>Email</a>.</>
|
<>Copy the link to this page, or share via <a href={`mailto:?subject=${encodeURIComponent(`Scheduling ${event?.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${id}`)}`}>email</a>.</>
|
||||||
}
|
}
|
||||||
</ShareInfo>
|
</ShareInfo>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
@ -230,7 +302,7 @@ const Event = (props) => {
|
||||||
id="timezone"
|
id="timezone"
|
||||||
inline
|
inline
|
||||||
value={timezone}
|
value={timezone}
|
||||||
onChange={value => setTimezone(value)}
|
onChange={event => setTimezone(event.currentTarget.value)}
|
||||||
options={timezones}
|
options={timezones}
|
||||||
/>
|
/>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
@ -272,8 +344,9 @@ const Event = (props) => {
|
||||||
<Center>Hover or tap the calendar below to see who is available</Center>
|
<Center>Hover or tap the calendar below to see who is available</Center>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
<AvailabilityViewer
|
<AvailabilityViewer
|
||||||
dates={event?.dates ?? []}
|
times={times}
|
||||||
times={event?.times ?? []}
|
timeLabels={timeLabels}
|
||||||
|
dates={dates}
|
||||||
people={people.filter(p => p.availability.length > 0)}
|
people={people.filter(p => p.availability.length > 0)}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
|
|
@ -285,20 +358,23 @@ const Event = (props) => {
|
||||||
<Center>Click and drag the calendar below to set your availabilities</Center>
|
<Center>Click and drag the calendar below to set your availabilities</Center>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
<AvailabilityEditor
|
<AvailabilityEditor
|
||||||
dates={event?.dates ?? []}
|
times={times}
|
||||||
times={event?.times ?? []}
|
timeLabels={timeLabels}
|
||||||
|
dates={dates}
|
||||||
value={user.availability}
|
value={user.availability}
|
||||||
onChange={async availability => {
|
onChange={async availability => {
|
||||||
const oldAvailability = [...user.availability];
|
const oldAvailability = [...user.availability];
|
||||||
|
const utcAvailability = availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'));
|
||||||
setUser({ ...user, availability });
|
setUser({ ...user, availability });
|
||||||
const response = await api.patch(`/event/${id}/people/${user.name}`, {
|
try {
|
||||||
person: {
|
await api.patch(`/event/${id}/people/${user.name}`, {
|
||||||
password,
|
person: {
|
||||||
availability,
|
password,
|
||||||
},
|
availability: utcAvailability,
|
||||||
});
|
},
|
||||||
if (response.status !== 200) {
|
});
|
||||||
console.log(response);
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
setUser({ ...user, oldAvailability });
|
setUser({ ...user, oldAvailability });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export const EventName = styled.h1`
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 300px;
|
width: 400px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: ${props.theme.loading};
|
background-color: ${props.theme.loading};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -83,7 +83,7 @@ export const ShareInfo = styled.p`
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 500px;
|
width: 300px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
background-color: ${props.theme.loading};
|
background-color: ${props.theme.loading};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useForm } from 'react-hook-form';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TextField,
|
TextField,
|
||||||
|
|
@ -40,6 +41,7 @@ import timezones from 'res/timezones.json';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { register, handleSubmit } = useForm({
|
const { register, handleSubmit } = useForm({
|
||||||
|
|
@ -73,18 +75,49 @@ const Home = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const times = JSON.parse(data.times);
|
const { start, end } = JSON.parse(data.times);
|
||||||
const dates = JSON.parse(data.dates);
|
const dates = JSON.parse(data.dates);
|
||||||
|
|
||||||
const start = dayjs().tz(data.timezone).hour(times.start);
|
if (dates.length === 0) {
|
||||||
const end = dayjs().tz(data.timezone).hour(times.end);
|
return setError(`You haven't selected any dates!`);
|
||||||
|
}
|
||||||
|
if (start === end) {
|
||||||
|
return setError(`The start and end times can't be the same`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let times = dates.reduce((times, date) => {
|
||||||
|
let day = [];
|
||||||
|
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||||
|
day.push(
|
||||||
|
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||||
|
.hour(i)
|
||||||
|
.minute(0)
|
||||||
|
.utc()
|
||||||
|
.format('HHmm-DDMMYYYY')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (start > end) {
|
||||||
|
for (let i = 0; i < end; i++) {
|
||||||
|
day.push(
|
||||||
|
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||||
|
.hour(i)
|
||||||
|
.minute(0)
|
||||||
|
.utc()
|
||||||
|
.format('HHmm-DDMMYYYY')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...times, ...day];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (times.length === 0) {
|
||||||
|
return setError(`You don't have any time selected`);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await api.post('/event', {
|
const response = await api.post('/event', {
|
||||||
event: {
|
event: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
startTime: start.utc().hour(),
|
times: times,
|
||||||
endTime: end.utc().hour(),
|
|
||||||
dates: dates,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
push(`/${response.data.id}`);
|
push(`/${response.data.id}`);
|
||||||
|
|
@ -164,7 +197,7 @@ const Home = () => {
|
||||||
<StatLabel>Events created</StatLabel>
|
<StatLabel>Events created</StatLabel>
|
||||||
</Stat>
|
</Stat>
|
||||||
<Stat>
|
<Stat>
|
||||||
<StatNumber>{stats.peopleCount ?? '10+'}</StatNumber>
|
<StatNumber>{stats.personCount ?? '10+'}</StatNumber>
|
||||||
<StatLabel>Availabilities entered</StatLabel>
|
<StatLabel>Availabilities entered</StatLabel>
|
||||||
</Stat>
|
</Stat>
|
||||||
</Stats>
|
</Stats>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue