diff --git a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
new file mode 100644
index 0000000..4254ab5
--- /dev/null
+++ b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
@@ -0,0 +1,112 @@
+import { useState, useRef, Fragment } from 'react';
+import dayjs from 'dayjs';
+import localeData from 'dayjs/plugin/localeData';
+import customParseFormat from 'dayjs/plugin/customParseFormat';
+
+import {
+ Wrapper,
+ Container,
+ Date,
+ DateLabel,
+ DayLabel,
+ Spacer,
+ TimeLabels,
+ TimeLabel,
+ TimeSpace,
+} from 'components/AvailabilityViewer/availabilityViewerStyle';
+import { Time } from './availabilityEditorStyle';
+
+dayjs.extend(localeData);
+dayjs.extend(customParseFormat);
+
+const AvailabilityEditor = ({
+ dates,
+ times,
+ value = [],
+ onChange,
+ ...props
+}) => {
+ const [selectingTimes, _setSelectingTimes] = useState([]);
+ const staticSelectingTimes = useRef([]);
+ const setSelectingTimes = newTimes => {
+ staticSelectingTimes.current = newTimes;
+ _setSelectingTimes(newTimes);
+ };
+
+ const startPos = useRef({});
+ const staticMode = useRef(null);
+ const [mode, _setMode] = useState(staticMode.current);
+ const setMode = newMode => {
+ staticMode.current = newMode;
+ _setMode(newMode);
+ };
+
+ return (
+
+
+
+ {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')} }
+
+ )}
+
+ {dates.map((date, x) => {
+ const parsedDate = dayjs(date, 'DDMMYYYY');
+ 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) =>
+ {
+ e.preventDefault();
+ startPos.current = {x, y};
+ setMode(value.includes(`${time}-${date}`) ? 'remove' : 'add');
+ setSelectingTimes([`${time}-${date}`]);
+ 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.map(d => `${times[d.y]}-${dates[d.x]}`));
+ }
+ }}
+ />
+ )}
+
+ {last && dates.length !== x+1 && (
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
+
+export default AvailabilityEditor;
diff --git a/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts b/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts
new file mode 100644
index 0000000..0046eb1
--- /dev/null
+++ b/crabfit-frontend/src/components/AvailabilityEditor/availabilityEditorStyle.ts
@@ -0,0 +1,21 @@
+import styled from '@emotion/styled';
+
+export const Time = styled.div`
+ height: 10px;
+ border-left: 1px solid ${props => props.theme.primaryDark};
+ touch-action: none;
+
+ ${props => props.time.slice(-2) === '00' && `
+ border-top: 1px solid ${props.theme.primaryDark};
+ `}
+ ${props => props.time.slice(-2) === '30' && `
+ border-top: 1px dotted ${props.theme.primaryDark};
+ `}
+
+ ${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
+ background-color: ${props.theme.primary};
+ `};
+ ${props => props.mode === 'remove' && props.selecting && `
+ background-color: initial;
+ `};
+`;
diff --git a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
index 31a6e9b..24d67e6 100644
--- a/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
+++ b/crabfit-frontend/src/components/AvailabilityViewer/AvailabilityViewer.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, Fragment } from 'react';
import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import customParseFormat from 'dayjs/plugin/customParseFormat';
@@ -15,6 +15,9 @@ import {
TooltipTitle,
TooltipDate,
TooltipContent,
+ TimeLabels,
+ TimeLabel,
+ TimeSpace,
} from './availabilityViewerStyle';
dayjs.extend(localeData);
@@ -31,12 +34,19 @@ const AvailabilityViewer = ({
return (
+
+ {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')} }
+
+ )}
+
{dates.map((date, i) => {
const parsedDate = dayjs(date, 'DDMMYYYY');
const last = dates.length === i+1 || dayjs(dates[i+1], 'DDMMYYYY').diff(parsedDate, 'day') > 1;
return (
- <>
-
+
+
{parsedDate.format('MMM D')}
{parsedDate.format('ddd')}
@@ -53,8 +63,8 @@ const AvailabilityViewer = ({
onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect();
setTooltip({
- x: Math.round(cellBox.x + cellBox.width),
- y: Math.round(cellBox.y + cellBox.height),
+ 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(', '),
@@ -70,7 +80,7 @@ const AvailabilityViewer = ({
{last && dates.length !== i+1 && (
)}
- >
+
);
})}
diff --git a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
index da02c15..15e9103 100644
--- a/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
+++ b/crabfit-frontend/src/components/AvailabilityViewer/availabilityViewerStyle.ts
@@ -9,8 +9,12 @@ export const Container = styled.div`
display: inline-flex;
box-sizing: border-box;
min-width: 100%;
- align-items: flex-start;
+ align-items: flex-end;
padding: 0 calc(calc(100% - 600px) / 2);
+
+ @media (max-width: 660px) {
+ padding: 0 30px;
+ }
`;
export const Date = styled.div`
@@ -19,6 +23,7 @@ export const Date = styled.div`
flex-direction: column;
width: 60px;
min-width: 60px;
+ margin-bottom: 10px;
& .time:last-of-type {
border-bottom: 1px solid ${props => props.theme.primaryDark};
@@ -63,13 +68,15 @@ export const Spacer = styled.div`
export const Tooltip = styled.div`
position: fixed;
- top: ${props => props.y+6}px;
- left: ${props => props.x+6}px;
+ top: ${props => props.y}px;
+ left: ${props => props.x}px;
+ transform: translateX(-50%);
border: 1px solid ${props => props.theme.text};
border-radius: 3px;
padding: 4px 8px;
- background-color: ${props => props.theme.background};
+ background-color: ${props => props.theme.background}99;
max-width: 200px;
+ pointer-events: none;
`;
export const TooltipTitle = styled.span`
@@ -89,3 +96,33 @@ export const TooltipContent = styled.span`
font-size: 13px;
display: block;
`;
+
+export const TimeLabels = styled.div`
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ width: 40px;
+ padding-right: 6px;
+`;
+
+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;
+ `}
+`;
+
+export const TimeLabel = styled.label`
+ display: block;
+ position: absolute;
+ top: -.7em;
+ font-size: 12px;
+ text-align: right;
+ user-select: none;
+ width: 100%;
+`;
diff --git a/crabfit-frontend/src/components/Donate/Donate.tsx b/crabfit-frontend/src/components/Donate/Donate.tsx
index 6555e62..4c28fc6 100644
--- a/crabfit-frontend/src/components/Donate/Donate.tsx
+++ b/crabfit-frontend/src/components/Donate/Donate.tsx
@@ -6,6 +6,7 @@ const Donate = () => (
Donate
diff --git a/crabfit-frontend/src/components/index.ts b/crabfit-frontend/src/components/index.ts
index 707a37f..5b31173 100644
--- a/crabfit-frontend/src/components/index.ts
+++ b/crabfit-frontend/src/components/index.ts
@@ -6,6 +6,7 @@ export { default as TimeRangeField } from './TimeRangeField/TimeRangeField';
export { default as Button } from './Button/Button';
export { default as Legend } from './Legend/Legend';
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer';
+export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor';
export { default as Center } from './Center/Center';
export { default as Donate } from './Donate/Donate';
diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx
index 71f18ef..7a52090 100644
--- a/crabfit-frontend/src/pages/Event/Event.tsx
+++ b/crabfit-frontend/src/pages/Event/Event.tsx
@@ -10,6 +10,7 @@ import {
Button,
Legend,
AvailabilityViewer,
+ AvailabilityEditor,
} from 'components';
import {
@@ -33,7 +34,11 @@ const Event = (props) => {
const { register, handleSubmit } = useForm();
const id = props.match.params.id;
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
- const [tab, setTab] = useState('group');
+ const [user, setUser] = useState({
+ name: 'Benji',
+ availability: [],
+ });
+ const [tab, setTab] = useState(user ? 'you' : 'group');
const onSubmit = data => console.log('submit', data);
@@ -49,36 +54,40 @@ const Event = (props) => {
Event name ({id})
https://page.url
- Copy the link to this page, or share via Email or Facebook .
+ Copy the link to this page, or share via Email or Facebook .
- Sign in to add your availability
-
-
+ {!user && (
+ <>
+ Sign in to add your availability
+
+
-
+
- Login
-
- These details are only for this event. Use a password to prevent others from changing your availability.
+ Login
+
+ These details are only for this event. Use a password to prevent others from changing your availability.
+ >
+ )}
{
id="timezone"
inline
value={timezone}
+ onChange={value => setTimezone(value)}
options={timezones}
/>
@@ -97,13 +107,13 @@ const Event = (props) => {
href="#you"
onClick={e => {
e.preventDefault();
- if (false) {
+ if (user) {
setTab('you');
}
}}
selected={tab === 'you'}
- disabled={true}
- title={true ? 'Login to set your availability' : ''}
+ disabled={!user}
+ title={user ? '' : 'Login to set your availability'}
>Your availability
{
{tab === 'group' ? (
-
- Hover and click the calendar below to see who is available
+
+ Hover or tap the calendar below to see who is available
{
'1400-08032021',
'1430-08032021',
],
- }]}
+ },{
+ name: 'Phoebe',
+ availability: [
+ '1100-07032021',
+ '1115-07032021',
+ '1130-07032021',
+ '1145-07032021',
+ '1200-07032021',
+ '1215-07032021',
+ '1230-07032021',
+ '1245-07032021',
+ '1300-07032021',
+ '1315-07032021',
+ '1330-07032021',
+ '1345-07032021',
+ '1400-07032021',
+ ],
+ },user]}
/>
) : (
@@ -183,6 +177,12 @@ const Event = (props) => {
Click and drag the calendar below to set your availabilities
+ setUser({ ...user, availability })}
+ />
)}
diff --git a/crabfit-frontend/src/pages/Event/eventStyle.ts b/crabfit-frontend/src/pages/Event/eventStyle.ts
index 0723f4f..62a6ede 100644
--- a/crabfit-frontend/src/pages/Event/eventStyle.ts
+++ b/crabfit-frontend/src/pages/Event/eventStyle.ts
@@ -8,7 +8,7 @@ export const StyledMain = styled.div`
export const Footer = styled.footer`
width: 600px;
- margin: 20px auto;
+ margin: 50px auto 20px;
max-width: calc(100% - 60px);
display: flex;
align-items: center;
diff --git a/crabfit-frontend/src/theme/index.ts b/crabfit-frontend/src/theme/index.ts
index a35a3c0..29f9f72 100644
--- a/crabfit-frontend/src/theme/index.ts
+++ b/crabfit-frontend/src/theme/index.ts
@@ -10,7 +10,7 @@ const theme = {
},
dark: {
mode: 'dark',
- background: '#111',
+ background: '#111111',
text: '#DDDDDD',
primary: '#F79E00',
primaryDark: '#F4BB60',