diff --git a/crabfit-backend/routes/createEvent.js b/crabfit-backend/routes/createEvent.js index 72de815..ef821ff 100644 --- a/crabfit-backend/routes/createEvent.js +++ b/crabfit-backend/routes/createEvent.js @@ -3,7 +3,7 @@ const dayjs = require('dayjs'); const adjectives = require('../res/adjectives.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 id = name.trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-'); @@ -12,7 +12,7 @@ const generateId = (name) => { }; 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) => { @@ -28,9 +28,7 @@ module.exports = async (req, res) => { data: { name: name, created: currentTime, - startTime: event.startTime, - endTime: event.endTime, - dates: event.dates, + times: event.times, }, }; @@ -40,9 +38,7 @@ module.exports = async (req, res) => { id: eventId, name: name, created: currentTime, - startTime: event.startTime, - endTime: event.endTime, - dates: event.dates, + times: event.times, }); } catch (e) { console.error(e); diff --git a/crabfit-backend/routes/stats.js b/crabfit-backend/routes/stats.js index 25a7363..921adf6 100644 --- a/crabfit-backend/routes/stats.js +++ b/crabfit-backend/routes/stats.js @@ -5,10 +5,11 @@ module.exports = async (req, res) => { let personCount = null; 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; - personCount = (await req.datastore.runQuery(query.filter('kind_name', 'Person')))[0][0].count; + eventCount = (await req.datastore.runQuery(eventQuery))[0][0].count; + personCount = (await req.datastore.runQuery(personQuery))[0][0].count; } catch (e) { console.error(e); } diff --git a/crabfit-backend/swagger.yaml b/crabfit-backend/swagger.yaml index 1794145..5dee8c1 100644 --- a/crabfit-backend/swagger.yaml +++ b/crabfit-backend/swagger.yaml @@ -18,11 +18,7 @@ definitions: type: "string" created: type: "integer" - startTime: - type: "string" - endTime: - type: "string" - dates: + times: type: "array" items: type: "string" @@ -82,11 +78,7 @@ paths: properties: name: type: "string" - startTime: - type: "integer" - endTime: - type: "integer" - dates: + times: type: "array" items: type: "string" diff --git a/crabfit-frontend/public/favicon.ico b/crabfit-frontend/public/favicon.ico index 093c5b0..bf79c38 100644 Binary files a/crabfit-frontend/public/favicon.ico and b/crabfit-frontend/public/favicon.ico differ diff --git a/crabfit-frontend/public/logo192.png b/crabfit-frontend/public/logo192.png index c011b9b..70f0a2c 100644 Binary files a/crabfit-frontend/public/logo192.png and b/crabfit-frontend/public/logo192.png differ diff --git a/crabfit-frontend/public/logo512.png b/crabfit-frontend/public/logo512.png index 5fba454..82acb67 100644 Binary files a/crabfit-frontend/public/logo512.png and b/crabfit-frontend/public/logo512.png differ diff --git a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx index 6e38ebd..621f1e0 100644 --- a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx +++ b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx @@ -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 = ({ - {!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) => - - {time.slice(-2) === '00' && {dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}} + {!!timeLabels.length && timeLabels.map((label, i) => + + {label.label?.length !== '' && {label.label}} )} @@ -56,47 +58,59 @@ const AvailabilityEditor = ({ const last = dates.length === x+1 || dayjs(dates[x+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1; return ( - + {parsedDate.format('MMM D')} {parsedDate.format('ddd')} - {times.map((time, y) => - {last && dates.length !== x+1 && ( diff --git a/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts b/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts index 0046eb1..6e43d71 100644 --- a/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts +++ b/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts @@ -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}; `}; `; diff --git a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx index 2e4904f..1a3e77e 100644 --- a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx +++ b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx @@ -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 = ({ - {!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) => - - {time.slice(-2) === '00' && {dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}} + {!!timeLabels.length && timeLabels.map((label, i) => + + {label.label?.length !== '' && {label.label}} )} @@ -48,38 +50,47 @@ const AvailabilityViewer = ({ const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1; return ( - + {parsedDate.format('MMM D')} {parsedDate.format('ddd')} - {times.map((time, i) => { - 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 ( + + ); + } + const time = `${timeLabel.time}-${date}`; + const peopleHere = people.filter(person => person.availability.includes(time)).map(person => person.name); - return ( - {last && dates.length !== i+1 && ( diff --git a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts index 907ddac..4023a30 100644 --- a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts +++ b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts @@ -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` diff --git a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts index 1789310..e415def 100644 --- a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts +++ b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts @@ -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'}; diff --git a/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx b/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx index fb5fce3..d044b4d 100644 --- a/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx +++ b/crabfit-frontend/src/components/TimeRangeField/TimeRangeField.tsx @@ -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} /> - end ? end : start} end={start > end ? start : end} /> + end ? 24 : end} /> + {start > end && end ? 0 : start} end={end} />} props.end * 4.1666666666666666}%); top: 0; background-color: ${props => props.theme.primary}; + border-radius: 2px; `; diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx index 45242a6..f82317d 100644 --- a/crabfit-frontend/src/pages/Event/Event.tsx +++ b/crabfit-frontend/src/pages/Event/Event.tsx @@ -1,6 +1,11 @@ import { Link } from 'react-router-dom'; 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 { Center, @@ -33,6 +38,10 @@ import api from 'services'; import logo from 'res/logo.svg'; import timezones from 'res/timezones.json'; +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(customParseFormat); + const Event = (props) => { const { register, handleSubmit } = useForm(); const { id } = props.match.params; @@ -46,37 +55,19 @@ const Event = (props) => { const [event, setEvent] = useState(null); const [people, setPeople] = useState([]); + const [times, setTimes] = useState([]); + const [timeLabels, setTimeLabels] = useState([]); + const [dates, setDates] = useState([]); const [min, setMin] = 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(() => { const fetchEvent = async () => { try { 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({ - ...response.data, - times, - }); + setEvent(response.data); + document.title = `${response.data.name} | Crab Fit`; setIsLoading(false); } catch (e) { console.error(e); @@ -85,28 +76,49 @@ const Event = (props) => { }; fetchEvent(); - fetchPeople(); - }, [fetchPeople, id]); + }, [id]); 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') { 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(() => { - if (event && !!people.length) { - const datetimes = event.dates.reduce( - (dates, date) => { - let times = []; - event.times.forEach(time => { - times.push(`${time}-${date}`); - }); - return [...dates, ...times]; - } - , [] - ); - setMin(datetimes.reduce((min, time) => { + if (!!times.length && !!people.length) { + setMin(times.reduce((min, time) => { let total = people.reduce( (total, person) => person.availability.includes(time) ? total+1 : total, 0 @@ -115,7 +127,7 @@ const Event = (props) => { }, Infinity )); - setMax(datetimes.reduce((max, time) => { + setMax(times.reduce((max, time) => { let total = people.reduce( (total, person) => person.availability.includes(time) ? total+1 : total, 0 @@ -125,7 +137,63 @@ const Event = (props) => { -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 => { setIsLoginLoading(true); @@ -137,7 +205,11 @@ const Event = (props) => { }, }); 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'); } catch (e) { if (e.status === 401) { @@ -178,10 +250,10 @@ const Event = (props) => { {event?.name} - {!!event?.name && `https://crab.fit/${id}`} + https://crab.fit/{id} {!!event?.name && - <>Copy the link to this page, or share via Email. + <>Copy the link to this page, or share via email. } @@ -230,7 +302,7 @@ const Event = (props) => { id="timezone" inline value={timezone} - onChange={value => setTimezone(value)} + onChange={event => setTimezone(event.currentTarget.value)} options={timezones} /> @@ -272,8 +344,9 @@ const Event = (props) => {
Hover or tap the calendar below to see who is available
p.availability.length > 0)} min={min} max={max} @@ -285,20 +358,23 @@ const Event = (props) => {
Click and drag the calendar below to set your availabilities
{ const oldAvailability = [...user.availability]; + const utcAvailability = availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY')); setUser({ ...user, availability }); - const response = await api.patch(`/event/${id}/people/${user.name}`, { - person: { - password, - availability, - }, - }); - if (response.status !== 200) { - console.log(response); + try { + await api.patch(`/event/${id}/people/${user.name}`, { + person: { + password, + availability: utcAvailability, + }, + }); + } catch (e) { + console.log(e); setUser({ ...user, oldAvailability }); } }} diff --git a/crabfit-frontend/src/pages/Event/eventStyle.ts b/crabfit-frontend/src/pages/Event/eventStyle.ts index f082645..5cc8afd 100644 --- a/crabfit-frontend/src/pages/Event/eventStyle.ts +++ b/crabfit-frontend/src/pages/Event/eventStyle.ts @@ -40,7 +40,7 @@ export const EventName = styled.h1` content: ''; display: inline-block; height: 1em; - width: 300px; + width: 400px; max-width: 100%; background-color: ${props.theme.loading}; border-radius: 3px; @@ -83,7 +83,7 @@ export const ShareInfo = styled.p` content: ''; display: inline-block; height: 1em; - width: 500px; + width: 300px; max-width: 100%; background-color: ${props.theme.loading}; border-radius: 3px; diff --git a/crabfit-frontend/src/pages/Home/Home.tsx b/crabfit-frontend/src/pages/Home/Home.tsx index dd860d8..b160592 100644 --- a/crabfit-frontend/src/pages/Home/Home.tsx +++ b/crabfit-frontend/src/pages/Home/Home.tsx @@ -5,6 +5,7 @@ import { useForm } from 'react-hook-form'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; import { TextField, @@ -40,6 +41,7 @@ import timezones from 'res/timezones.json'; dayjs.extend(utc); dayjs.extend(timezone); +dayjs.extend(customParseFormat); const Home = () => { const { register, handleSubmit } = useForm({ @@ -73,18 +75,49 @@ const Home = () => { setIsLoading(true); setError(null); try { - const times = JSON.parse(data.times); + const { start, end } = JSON.parse(data.times); const dates = JSON.parse(data.dates); - const start = dayjs().tz(data.timezone).hour(times.start); - const end = dayjs().tz(data.timezone).hour(times.end); + if (dates.length === 0) { + 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', { event: { name: data.name, - startTime: start.utc().hour(), - endTime: end.utc().hour(), - dates: dates, + times: times, }, }); push(`/${response.data.id}`); @@ -164,7 +197,7 @@ const Home = () => { Events created - {stats.peopleCount ?? '10+'} + {stats.personCount ?? '10+'} Availabilities entered