Calendar field and time field
This commit is contained in:
parent
edcd4dcaa0
commit
0dde47109f
32 changed files with 901 additions and 65 deletions
192
crabfit-frontend/src/components/CalendarField/CalendarField.tsx
Normal file
192
crabfit-frontend/src/components/CalendarField/CalendarField.tsx
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
|
||||
import { Button } from 'components';
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
CalendarHeader,
|
||||
CalendarBody,
|
||||
Date,
|
||||
Day,
|
||||
} from './calendarFieldStyle';
|
||||
|
||||
dayjs.extend(isToday);
|
||||
|
||||
const days = [
|
||||
'Sun',
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
'Thu',
|
||||
'Fri',
|
||||
'Sat',
|
||||
];
|
||||
|
||||
const months = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
const calculateMonth = (month, year) => {
|
||||
const date = dayjs().month(month).year(year);
|
||||
const daysInMonth = date.daysInMonth();
|
||||
const daysBefore = date.date(1).day();
|
||||
const daysAfter = 6 - date.date(daysInMonth).day();
|
||||
|
||||
let dates = [];
|
||||
let curDate = date.date(1).subtract(daysBefore, 'day');
|
||||
let y = 0;
|
||||
let x = 0;
|
||||
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
||||
if (x === 0) dates[y] = [];
|
||||
dates[y][x] = curDate.clone();
|
||||
curDate = curDate.add(1, 'day');
|
||||
x++;
|
||||
if (x > 6) {
|
||||
x = 0;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
|
||||
return dates;
|
||||
};
|
||||
|
||||
const CalendarField = ({
|
||||
label,
|
||||
subLabel,
|
||||
id,
|
||||
register,
|
||||
...props
|
||||
}) => {
|
||||
const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year()));
|
||||
const [month, setMonth] = useState(dayjs().month());
|
||||
const [year, setYear] = useState(dayjs().year());
|
||||
|
||||
const [selectedDates, setSelectedDates] = useState([]);
|
||||
const [selectingDates, _setSelectingDates] = useState([]);
|
||||
const staticSelectingDates = useRef([]);
|
||||
const setSelectingDates = newDates => {
|
||||
staticSelectingDates.current = newDates;
|
||||
_setSelectingDates(newDates);
|
||||
};
|
||||
|
||||
const startPos = useRef({});
|
||||
const staticMode = useRef(null);
|
||||
const [mode, _setMode] = useState(staticMode.current);
|
||||
const setMode = newMode => {
|
||||
staticMode.current = newMode;
|
||||
_setMode(newMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDates(calculateMonth(month, year));
|
||||
}, [month, year]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<input
|
||||
id={id}
|
||||
type="hidden"
|
||||
ref={register}
|
||||
value={JSON.stringify(selectedDates)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<CalendarHeader>
|
||||
<Button
|
||||
buttonHeight="30px"
|
||||
buttonWidth="30px"
|
||||
padding="0"
|
||||
title="Previous month"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (month-1 < 0) {
|
||||
setYear(year-1);
|
||||
setMonth(11);
|
||||
} else {
|
||||
setMonth(month-1);
|
||||
}
|
||||
}}
|
||||
><</Button>
|
||||
<span>{months[month]} {year}</span>
|
||||
<Button
|
||||
buttonHeight="30px"
|
||||
buttonWidth="30px"
|
||||
padding="0"
|
||||
title="Next month"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (month+1 > 11) {
|
||||
setYear(year+1);
|
||||
setMonth(0);
|
||||
} else {
|
||||
setMonth(month+1);
|
||||
}
|
||||
}}
|
||||
>></Button>
|
||||
</CalendarHeader>
|
||||
|
||||
<CalendarBody>
|
||||
{days.map((name, i) =>
|
||||
<Day key={i}>{name}</Day>
|
||||
)}
|
||||
{dates.length > 0 && dates.map((dateRow, y) =>
|
||||
dateRow.map((date, x) =>
|
||||
<Date
|
||||
key={y+x}
|
||||
otherMonth={date.month() !== month}
|
||||
isToday={date.isToday()}
|
||||
title={`${date.date()} ${months[date.month()]}${date.isToday() ? ' (today)' : ''}`}
|
||||
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||
selecting={selectingDates.includes(date)}
|
||||
mode={mode}
|
||||
onMouseDown={() => {
|
||||
startPos.current = {x, y};
|
||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
||||
setSelectingDates([date]);
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
|
||||
} else if (staticMode.current === 'remove') {
|
||||
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
|
||||
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
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});
|
||||
}
|
||||
}
|
||||
setSelectingDates(found.map(d => dates[d.y][d.x]));
|
||||
}
|
||||
}}
|
||||
>{date.date()}</Date>
|
||||
)
|
||||
)}
|
||||
</CalendarBody>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarField;
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 30px 0;
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
|
||||
export const CalendarHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
padding: 6px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const CalendarBody = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
`;
|
||||
|
||||
export const Date = styled.div`
|
||||
background-color: ${props => props.theme.primary}22;
|
||||
border: 1px solid ${props => props.theme.primaryLight};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
|
||||
${props => props.otherMonth && `
|
||||
color: ${props.theme.primaryLight};
|
||||
`}
|
||||
${props => props.isToday && `
|
||||
font-weight: 900;
|
||||
color: ${props.theme.primaryDark};
|
||||
`}
|
||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||
background-color: ${props.theme.primary};
|
||||
border-color: ${props.theme.primary};
|
||||
`}
|
||||
${props => props.mode === 'remove' && props.selecting && `
|
||||
background-color: ${props.theme.primary}22;
|
||||
border: 1px solid ${props.theme.primaryLight};
|
||||
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Day = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 10px;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
opacity: .7;
|
||||
`;
|
||||
Loading…
Add table
Add a link
Reference in a new issue