diff --git a/crabfit-backend/routes/createEvent.js b/crabfit-backend/routes/createEvent.js index 4e54479..08562f3 100644 --- a/crabfit-backend/routes/createEvent.js +++ b/crabfit-backend/routes/createEvent.js @@ -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); diff --git a/crabfit-backend/swagger.yaml b/crabfit-backend/swagger.yaml index 750b507..f9656a6 100644 --- a/crabfit-backend/swagger.yaml +++ b/crabfit-backend/swagger.yaml @@ -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: diff --git a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx index 37a6ac1..81dfabe 100644 --- a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx +++ b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx @@ -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 ( - - - - {!!timeLabels.length && timeLabels.map((label, i) => - - {label.label?.length !== '' && {label.label}} - - )} - - {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 ( - - - {isSpecificDates && {parsedDate.format('MMM D')}} - {parsedDate.format('ddd')} + <> + + p.availability.length > 0).length} + onSegmentFocus={count => setFocusCount(count)} + /> +
Hover or tap the calendar below to see who is available
+ {!!people.length && ( + <> +
Click the names below to view people individually
+ + {people.map((person, i) => + { + 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} + )} + + + )} +
- - {timeLabels.map((timeLabel, i) => { - if (!timeLabel.time) return null; - if (!times.includes(`${timeLabel.time}-${date}`)) { - return ( - - ); - } - const time = `${timeLabel.time}-${date}`; - const peopleHere = people.filter(person => person.availability.includes(time)).map(person => person.name); + + + + {!!timeLabels.length && timeLabels.map((label, i) => + + {label.label?.length !== '' && {label.label}} + + )} + + {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 ( + + + {isSpecificDates && {parsedDate.format('MMM D')}} + {parsedDate.format('ddd')} - return ( - -
- {last && dates.length !== i+1 && ( - - )} -
- ); - })} -
- {tooltip && ( - - {tooltip.available} - {tooltip.date} - {tooltip.people} - - )} -
+ + {timeLabels.map((timeLabel, i) => { + if (!timeLabel.time) return null; + if (!times.includes(`${timeLabel.time}-${date}`)) { + return ( + + ); + } + 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 ( + + + {last && dates.length !== i+1 && ( + + )} + + ); + })} + + {tooltip && ( + + {tooltip.available} + {tooltip.date} + {tooltip.people} + + )} + + ); }; diff --git a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts index 1abf22a..707488f 100644 --- a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts +++ b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts @@ -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}; + `} +`; diff --git a/crabfit-frontend/src/components/Legend/Legend.tsx b/crabfit-frontend/src/components/Legend/Legend.tsx index 91b8645..16a51e3 100644 --- a/crabfit-frontend/src/components/Legend/Legend.tsx +++ b/crabfit-frontend/src/components/Legend/Legend.tsx @@ -11,6 +11,7 @@ const Legend = ({ min, max, total, + onSegmentFocus, ...props }) => { const theme = useTheme(); @@ -19,9 +20,13 @@ const Legend = ({ - + onSegmentFocus(null)}> {[...Array(max-min+1).keys()].map(i => - + onSegmentFocus(i+min)} + /> )} diff --git a/crabfit-frontend/src/pages/Create/Create.tsx b/crabfit-frontend/src/pages/Create/Create.tsx index c5d600a..e221521 100644 --- a/crabfit-frontend/src/pages/Create/Create.tsx +++ b/crabfit-frontend/src/pages/Create/Create.tsx @@ -119,6 +119,7 @@ const Create = ({ offline }) => { event: { name: data.name, times: times, + timezone: data.timezone, }, }); setCreatedEvent(response.data); diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx index 382ba33..2d4c8e8 100644 --- a/crabfit-frontend/src/pages/Event/Event.tsx +++ b/crabfit-frontend/src/pages/Event/Event.tsx @@ -13,7 +13,6 @@ import { TextField, SelectField, Button, - Legend, AvailabilityViewer, AvailabilityEditor, Error, @@ -398,14 +397,6 @@ const Event = (props) => { {tab === 'group' ? (
- - p.availability.length > 0).length} - /> -
Hover or tap the calendar below to see who is available
-
{

Send the link to everyone you want to come.

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!

In this example, 1pm to 3pm on Friday the 16th works for all Jenny's friends.

- { event: { name: data.name, times: times, + timezone: data.timezone, }, }); push(`/${response.data.id}`);