commit
370adf5268
|
|
@ -7,6 +7,7 @@ module.exports = async (req, res) => {
|
|||
people = people.map(person => ({
|
||||
name: person.name,
|
||||
availability: person.availability,
|
||||
created: person.created,
|
||||
}));
|
||||
|
||||
res.send({
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ module.exports = async (req, res) => {
|
|||
res.send({
|
||||
name: personName,
|
||||
availability: personResult.availability,
|
||||
created: personResult.created,
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ definitions:
|
|||
type: "array"
|
||||
items:
|
||||
type: "string"
|
||||
created:
|
||||
type: "integer"
|
||||
paths:
|
||||
"/stats":
|
||||
get:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { useState, useEffect, Fragment } from 'react';
|
||||
import { useState, useEffect, useRef, Fragment } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import { useSettingsStore } from 'stores';
|
||||
|
||||
import { Legend, Center } from 'components';
|
||||
import {
|
||||
Wrapper,
|
||||
ScrollWrapper,
|
||||
Container,
|
||||
Date,
|
||||
Times,
|
||||
|
|
@ -19,6 +21,7 @@ import {
|
|||
TooltipTitle,
|
||||
TooltipDate,
|
||||
TooltipContent,
|
||||
TooltipPerson,
|
||||
TimeLabels,
|
||||
TimeLabel,
|
||||
TimeSpace,
|
||||
|
|
@ -29,6 +32,7 @@ import {
|
|||
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const AvailabilityViewer = ({
|
||||
times,
|
||||
|
|
@ -47,6 +51,8 @@ const AvailabilityViewer = ({
|
|||
const [tempFocus, setTempFocus] = useState(null);
|
||||
const [focusCount, setFocusCount] = useState(null);
|
||||
|
||||
const wrapper = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredPeople(people.map(p => p.name));
|
||||
setTouched(people.length <= 1);
|
||||
|
|
@ -85,6 +91,7 @@ const AvailabilityViewer = ({
|
|||
}}
|
||||
onMouseOver={() => setTempFocus(person.name)}
|
||||
onMouseOut={() => setTempFocus(null)}
|
||||
title={person.created && dayjs.unix(person.created).fromNow()}
|
||||
>{person.name}</Person>
|
||||
)}
|
||||
</People>
|
||||
|
|
@ -92,82 +99,94 @@ const AvailabilityViewer = ({
|
|||
)}
|
||||
</StyledMain>
|
||||
|
||||
<Wrapper>
|
||||
<Container>
|
||||
<TimeLabels>
|
||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||
<TimeSpace key={i}>
|
||||
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||
</TimeSpace>
|
||||
)}
|
||||
</TimeLabels>
|
||||
{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 (
|
||||
<Fragment key={i}>
|
||||
<Date>
|
||||
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
|
||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||
<Wrapper ref={wrapper}>
|
||||
<ScrollWrapper>
|
||||
<Container>
|
||||
<TimeLabels>
|
||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||
<TimeSpace key={i}>
|
||||
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
|
||||
</TimeSpace>
|
||||
)}
|
||||
</TimeLabels>
|
||||
{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 (
|
||||
<Fragment key={i}>
|
||||
<Date>
|
||||
{isSpecificDates && <DateLabel>{parsedDate.format('MMM D')}</DateLabel>}
|
||||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||
|
||||
<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 = 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);
|
||||
<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 = 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 (
|
||||
<Time
|
||||
key={i}
|
||||
time={time}
|
||||
className="time"
|
||||
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
|
||||
aria-label={peopleHere.join(', ')}
|
||||
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
||||
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
||||
onMouseEnter={(e) => {
|
||||
const cellBox = e.currentTarget.getBoundingClientRect();
|
||||
const timeText = timeFormat === '12h' ? 'h:mma' : 'HH:mm';
|
||||
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(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
||||
people: peopleHere.join(', '),
|
||||
});
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltip(null);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Times>
|
||||
</Date>
|
||||
{last && dates.length !== i+1 && (
|
||||
<Spacer />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
x={tooltip.x}
|
||||
y={tooltip.y}
|
||||
>
|
||||
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
||||
<TooltipDate>{tooltip.date}</TooltipDate>
|
||||
<TooltipContent>{tooltip.people}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
return (
|
||||
<Time
|
||||
key={i}
|
||||
time={time}
|
||||
className="time"
|
||||
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
|
||||
aria-label={peopleHere.join(', ')}
|
||||
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
||||
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
||||
onMouseEnter={(e) => {
|
||||
const cellBox = e.currentTarget.getBoundingClientRect();
|
||||
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
|
||||
const timeText = timeFormat === '12h' ? 'h:mma' : 'HH:mm';
|
||||
setTooltip({
|
||||
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
|
||||
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
|
||||
available: `${peopleHere.length} / ${people.length} available`,
|
||||
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
||||
people: peopleHere,
|
||||
});
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltip(null);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Times>
|
||||
</Date>
|
||||
{last && dates.length !== i+1 && (
|
||||
<Spacer />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
x={tooltip.x}
|
||||
y={tooltip.y}
|
||||
>
|
||||
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
||||
<TooltipDate>{tooltip.date}</TooltipDate>
|
||||
{!!filteredPeople.length && (
|
||||
<TooltipContent>
|
||||
{tooltip.people.map(person =>
|
||||
<TooltipPerson key={person}>{person}</TooltipPerson>
|
||||
)}
|
||||
{filteredPeople.filter(p => !tooltip.people.includes(p)).map(person =>
|
||||
<TooltipPerson key={person} disabled>{person}</TooltipPerson>
|
||||
)}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</ScrollWrapper>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
margin: 20px 0;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ScrollWrapper = styled.div`
|
||||
overflow-x: auto;
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
|
|
@ -62,8 +67,8 @@ export const Time = styled.div`
|
|||
`}
|
||||
|
||||
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)}`}
|
||||
${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`},
|
||||
${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`}
|
||||
);
|
||||
`;
|
||||
|
||||
|
|
@ -73,16 +78,17 @@ export const Spacer = styled.div`
|
|||
`;
|
||||
|
||||
export const Tooltip = styled.div`
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
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}DD;
|
||||
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
|
||||
max-width: 200px;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
export const TooltipTitle = styled.span`
|
||||
|
|
@ -94,13 +100,26 @@ export const TooltipTitle = styled.span`
|
|||
export const TooltipDate = styled.span`
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
opacity: .7;
|
||||
font-weight: 700;
|
||||
opacity: .8;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const TooltipContent = styled.span`
|
||||
export const TooltipContent = styled.div`
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
`;
|
||||
|
||||
export const TooltipPerson = styled.span`
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
padding: 1px 4px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
|
||||
${props => props.disabled && `
|
||||
opacity: .5;
|
||||
border-color: ${props.theme.text}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const TimeLabels = styled.div`
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ const Legend = ({
|
|||
<Label>{min}/{total} available</Label>
|
||||
|
||||
<Bar onMouseOut={() => onSegmentFocus(null)}>
|
||||
{[...Array(max-min+1).keys()].map(i =>
|
||||
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
||||
<Grade
|
||||
key={i}
|
||||
color={`${theme.primary}${Math.round((i/(max-min))*255).toString(16)}`}
|
||||
onMouseOver={() => onSegmentFocus(i+min)}
|
||||
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
||||
onMouseOver={() => onSegmentFocus(i)}
|
||||
/>
|
||||
)}
|
||||
</Bar>
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ const Event = (props) => {
|
|||
{(!!event || isLoading) ? (
|
||||
<>
|
||||
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||
<EventDate isLoading={isLoading} title={event?.created && dayjs.unix(event?.created).format('h:mma D MMMM, YYYY')}>{event?.created && `Created ${dayjs.unix(event?.created).fromNow()}`}</EventDate>
|
||||
<EventDate isLoading={isLoading} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && `Created ${dayjs.unix(event?.created).fromNow()}`}</EventDate>
|
||||
<ShareInfo
|
||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
|
||||
.then(() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue