🚧 Commence l'ajout de calendrier
This commit is contained in:
335
resources/js/components/calendar/CalendarHeatmap.tsx
Normal file
335
resources/js/components/calendar/CalendarHeatmap.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import React from 'react';
|
||||
import { DAYS_IN_WEEK, MILLISECONDS_IN_ONE_DAY, DAY_LABELS, MONTH_LABELS } from './constants.js';
|
||||
import {
|
||||
shiftDate,
|
||||
getBeginningTimeForDate,
|
||||
convertToDate,
|
||||
getRange,
|
||||
} from "../../utils";
|
||||
import {CalendarHeatmapProp} from "./CalendarHeatmapProp";
|
||||
|
||||
const SQUARE_SIZE = 10;
|
||||
const MONTH_LABEL_GUTTER_SIZE = 4;
|
||||
const CSS_PSEUDO_NAMESPACE = 'react-calendar-heatmap-';
|
||||
|
||||
function CalendarHeatmap({
|
||||
values, startDate, endDate, gutterSize, horizontal,
|
||||
showMonthLabels, showWeekdayLabels, showOutOfRangeDays, tooltipDataAttrs,
|
||||
onClick, onMouseOver, onMouseLeave, transformDayElement
|
||||
}: CalendarHeatmapProp) {
|
||||
|
||||
const cachedValues = {};
|
||||
for (const i in values) {
|
||||
const value = values[i];
|
||||
const date = convertToDate(value.date);
|
||||
const index = Math.floor((date.getTime() - getStartDateWithEmptyDays().getTime()) / MILLISECONDS_IN_ONE_DAY);
|
||||
cachedValues[index] = {
|
||||
date: value.date,
|
||||
event: value.event,
|
||||
className: classForValue(value.id),
|
||||
title: titleForValue(value.id),
|
||||
tooltipDataAttrs: getTooltipDataAttrsForValue(value.id),
|
||||
};
|
||||
}
|
||||
|
||||
function getDateDifferenceInDays() {
|
||||
const timeDiff = getEndDate().getTime() - convertToDate(startDate).getTime();
|
||||
return Math.ceil(timeDiff / MILLISECONDS_IN_ONE_DAY);
|
||||
}
|
||||
|
||||
function classForValue(value) {
|
||||
return value ? 'color-filled' : 'color-empty';
|
||||
}
|
||||
|
||||
function titleForValue(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function getSquareSizeWithGutter() {
|
||||
return SQUARE_SIZE + gutterSize;
|
||||
}
|
||||
|
||||
function getMonthLabelSize() {
|
||||
if (!showMonthLabels) {
|
||||
return 0;
|
||||
}
|
||||
if (horizontal) {
|
||||
return SQUARE_SIZE + MONTH_LABEL_GUTTER_SIZE;
|
||||
}
|
||||
return 2 * (SQUARE_SIZE + MONTH_LABEL_GUTTER_SIZE);
|
||||
}
|
||||
|
||||
function getWeekdayLabelSize() {
|
||||
if (!showWeekdayLabels) {
|
||||
return 0;
|
||||
}
|
||||
if (horizontal) {
|
||||
return 30;
|
||||
}
|
||||
return SQUARE_SIZE * 1.5;
|
||||
}
|
||||
|
||||
function getStartDate() {
|
||||
return shiftDate(getEndDate(), -getDateDifferenceInDays() + 1); // +1 because endDate is inclusive
|
||||
}
|
||||
|
||||
function getEndDate(): Date {
|
||||
return getBeginningTimeForDate(convertToDate(endDate));
|
||||
}
|
||||
|
||||
function getStartDateWithEmptyDays() {
|
||||
return shiftDate(getStartDate(), -getNumEmptyDaysAtStart());
|
||||
}
|
||||
|
||||
function getNumEmptyDaysAtStart() {
|
||||
return getStartDate().getDay();
|
||||
}
|
||||
|
||||
function getNumEmptyDaysAtEnd() {
|
||||
return DAYS_IN_WEEK - 1 - getEndDate().getDay();
|
||||
}
|
||||
|
||||
function getWeekCount() {
|
||||
const numDaysRoundedToWeek =
|
||||
getDateDifferenceInDays() + getNumEmptyDaysAtStart() + getNumEmptyDaysAtEnd();
|
||||
return Math.ceil(numDaysRoundedToWeek / DAYS_IN_WEEK);
|
||||
}
|
||||
|
||||
function getWeekWidth() {
|
||||
return DAYS_IN_WEEK * getSquareSizeWithGutter();
|
||||
}
|
||||
|
||||
function getWidth() {
|
||||
return (
|
||||
getWeekCount() * getSquareSizeWithGutter() -
|
||||
(gutterSize - getWeekdayLabelSize())
|
||||
);
|
||||
}
|
||||
|
||||
function getHeight() {
|
||||
return (
|
||||
getWeekWidth() +
|
||||
(getMonthLabelSize() - gutterSize) +
|
||||
getWeekdayLabelSize()
|
||||
);
|
||||
}
|
||||
|
||||
function getValueForIndex(index) {
|
||||
if (cachedValues[index]) {
|
||||
console.log(index, cachedValues);
|
||||
return cachedValues[index].event;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getClassNameForIndex(index) {
|
||||
if (cachedValues[index]) {
|
||||
return cachedValues[index].className;
|
||||
}
|
||||
return classForValue(null);
|
||||
}
|
||||
|
||||
function getTitleForIndex(index) {
|
||||
if (cachedValues[index]) {
|
||||
return cachedValues[index].title;
|
||||
}
|
||||
return titleForValue ? titleForValue(null) : null;
|
||||
}
|
||||
|
||||
function getTooltipDataAttrsForIndex(index) {
|
||||
if (cachedValues[index]) {
|
||||
return cachedValues[index].tooltipDataAttrs;
|
||||
}
|
||||
return getTooltipDataAttrsForValue({ date: null, count: null });
|
||||
}
|
||||
|
||||
function getTooltipDataAttrsForValue(value) {
|
||||
if (typeof tooltipDataAttrs === 'function') {
|
||||
return tooltipDataAttrs(value);
|
||||
}
|
||||
return tooltipDataAttrs;
|
||||
}
|
||||
|
||||
function getTransformForWeek(weekIndex) {
|
||||
if (horizontal) {
|
||||
return `translate(${weekIndex * getSquareSizeWithGutter()}, 0)`;
|
||||
}
|
||||
return `translate(0, ${weekIndex * getSquareSizeWithGutter()})`;
|
||||
}
|
||||
|
||||
function getTransformForWeekdayLabels() {
|
||||
if (horizontal) {
|
||||
return `translate(${SQUARE_SIZE}, ${getMonthLabelSize()})`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getTransformForMonthLabels() {
|
||||
if (horizontal) {
|
||||
return `translate(${getWeekdayLabelSize()}, 0)`;
|
||||
}
|
||||
return `translate(${getWeekWidth() + MONTH_LABEL_GUTTER_SIZE}, ${getWeekdayLabelSize()})`;
|
||||
}
|
||||
|
||||
function getTransformForAllWeeks() {
|
||||
if (horizontal) {
|
||||
return `translate(${getWeekdayLabelSize()}, ${getMonthLabelSize()})`;
|
||||
}
|
||||
return `translate(0, ${getWeekdayLabelSize()})`;
|
||||
}
|
||||
|
||||
function getViewBox() {
|
||||
if (horizontal) {
|
||||
return `0 0 ${getWidth()} ${getHeight()}`;
|
||||
}
|
||||
return `0 0 ${getHeight()} ${getWidth()}`;
|
||||
}
|
||||
|
||||
function getSquareCoordinates(dayIndex) {
|
||||
if (horizontal) {
|
||||
return [0, dayIndex * getSquareSizeWithGutter()];
|
||||
}
|
||||
return [dayIndex * getSquareSizeWithGutter(), 0];
|
||||
}
|
||||
|
||||
function getWeekdayLabelCoordinates(dayIndex) {
|
||||
if (horizontal) {
|
||||
return [0, (dayIndex + 1) * SQUARE_SIZE + dayIndex * gutterSize];
|
||||
}
|
||||
return [dayIndex * SQUARE_SIZE + dayIndex * gutterSize, SQUARE_SIZE];
|
||||
}
|
||||
|
||||
function getMonthLabelCoordinates(weekIndex) {
|
||||
if (horizontal) {
|
||||
return [
|
||||
weekIndex * getSquareSizeWithGutter(),
|
||||
getMonthLabelSize() - MONTH_LABEL_GUTTER_SIZE,
|
||||
];
|
||||
}
|
||||
const verticalOffset = -2;
|
||||
return [0, (weekIndex + 1) * getSquareSizeWithGutter() + verticalOffset];
|
||||
}
|
||||
|
||||
function handleClick(value) {
|
||||
if (onClick) {
|
||||
onClick(value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseOver(e, value) {
|
||||
if (onMouseOver) {
|
||||
onMouseOver(e, value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseLeave(e, value) {
|
||||
if (onMouseLeave) {
|
||||
onMouseLeave(e, value);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSquare(dayIndex, index) {
|
||||
const indexOutOfRange =
|
||||
index < getNumEmptyDaysAtStart() ||
|
||||
index >= getNumEmptyDaysAtStart() + getDateDifferenceInDays();
|
||||
if (indexOutOfRange && !showOutOfRangeDays) {
|
||||
return null;
|
||||
}
|
||||
const [x, y] = getSquareCoordinates(dayIndex);
|
||||
const value = getValueForIndex(index);
|
||||
const rect = (
|
||||
<rect
|
||||
key={index}
|
||||
width={SQUARE_SIZE}
|
||||
height={SQUARE_SIZE}
|
||||
x={x}
|
||||
y={y}
|
||||
className={getClassNameForIndex(index)}
|
||||
onClick={() => handleClick(value)}
|
||||
onMouseOver={(e) => handleMouseOver(e, value)}
|
||||
onMouseLeave={(e) => handleMouseLeave(e, value)}
|
||||
{...getTooltipDataAttrsForIndex(index)}
|
||||
>
|
||||
<title>{getTitleForIndex(index)}</title>
|
||||
</rect>
|
||||
);
|
||||
return transformDayElement ? transformDayElement(rect, value as string, index) : rect;
|
||||
}
|
||||
|
||||
function renderWeek(weekIndex) {
|
||||
return (
|
||||
<g
|
||||
key={weekIndex}
|
||||
transform={getTransformForWeek(weekIndex)}
|
||||
className={`${CSS_PSEUDO_NAMESPACE}week`}
|
||||
>
|
||||
{getRange(DAYS_IN_WEEK).map((dayIndex) =>
|
||||
renderSquare(dayIndex, weekIndex * DAYS_IN_WEEK + dayIndex),
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAllWeeks() {
|
||||
return getRange(getWeekCount()).map((weekIndex) => renderWeek(weekIndex));
|
||||
}
|
||||
|
||||
function renderMonthLabels() {
|
||||
if (!showMonthLabels) {
|
||||
return null;
|
||||
}
|
||||
const weekRange = getRange(getWeekCount() - 1); // don't render for last week, because label will be cut off
|
||||
return weekRange.map((weekIndex) => {
|
||||
const endOfWeek = shiftDate(getStartDateWithEmptyDays(), (weekIndex + 1) * DAYS_IN_WEEK);
|
||||
const [x, y] = getMonthLabelCoordinates(weekIndex);
|
||||
return endOfWeek.getDate() >= 1 && endOfWeek.getDate() <= DAYS_IN_WEEK ? (
|
||||
<text key={weekIndex} x={x} y={y} className={`${CSS_PSEUDO_NAMESPACE}month-label`}>
|
||||
{MONTH_LABELS[endOfWeek.getMonth()]}
|
||||
</text>
|
||||
) : null;
|
||||
});
|
||||
}
|
||||
|
||||
function renderWeekdayLabels() {
|
||||
if (!showWeekdayLabels) {
|
||||
return null;
|
||||
}
|
||||
return DAY_LABELS.map((weekdayLabel, dayIndex) => {
|
||||
const [x, y] = getWeekdayLabelCoordinates(dayIndex);
|
||||
const cssClasses = `${
|
||||
horizontal ? '' : `${CSS_PSEUDO_NAMESPACE}small-text`
|
||||
} ${CSS_PSEUDO_NAMESPACE}weekday-label`;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return dayIndex & 1 ? (
|
||||
<text key={`${x}${y}`} x={x} y={y} className={cssClasses}>
|
||||
{weekdayLabel}
|
||||
</text>
|
||||
) : null;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<svg className="react-calendar-heatmap" viewBox={getViewBox()}>
|
||||
<g
|
||||
transform={getTransformForMonthLabels()}
|
||||
className={`${CSS_PSEUDO_NAMESPACE}month-labels`}
|
||||
>
|
||||
{renderMonthLabels()}
|
||||
</g>
|
||||
<g
|
||||
transform={getTransformForAllWeeks()}
|
||||
className={`${CSS_PSEUDO_NAMESPACE}all-weeks`}
|
||||
>
|
||||
{renderAllWeeks()}
|
||||
</g>
|
||||
<g
|
||||
transform={getTransformForWeekdayLabels()}
|
||||
className={`${CSS_PSEUDO_NAMESPACE}weekday-labels`}
|
||||
>
|
||||
{renderWeekdayLabels()}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default CalendarHeatmap;
|
19
resources/js/components/calendar/CalendarHeatmapProp.ts
Normal file
19
resources/js/components/calendar/CalendarHeatmapProp.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {CalendarHeatmapValuesProp} from "./CalendarHeatmapValuesProp";
|
||||
import {ReactEventHandler} from "react";
|
||||
|
||||
export interface CalendarHeatmapProp {
|
||||
values: CalendarHeatmapValuesProp[]; // array of objects with date and arbitrary metadata
|
||||
startDate: Date; // start of date range
|
||||
endDate: Date; // end of date range
|
||||
gutterSize: number; // size of space between squares
|
||||
horizontal: boolean; // whether to orient horizontally or vertically
|
||||
showMonthLabels: boolean; // whether to show month labels
|
||||
showWeekdayLabels: boolean; // whether to show weekday labels
|
||||
showOutOfRangeDays: boolean; // whether to render squares for extra days in week after endDate, and before start date
|
||||
tooltipDataAttrs: object; // data attributes to add to square for setting 3rd party tooltips, e.g. { 'data-toggle': 'tooltip' } for bootstrap tooltips
|
||||
onClick: (v:string) => void; // callback function when a square is clicked
|
||||
onMouseOver?: (e:ReactEventHandler, v:string) => void; // callback function when mouse pointer is over a square
|
||||
onMouseLeave?: (e:ReactEventHandler, v:string) => void; // callback function when mouse pointer is left a square
|
||||
transformDayElement?: (element:JSX.Element, v:string, i:number) => void; // function to further transform the svg element for a single day
|
||||
}
|
||||
|
@@ -0,0 +1,5 @@
|
||||
export interface CalendarHeatmapValuesProp {
|
||||
id: string;
|
||||
date: Date;
|
||||
event: string;
|
||||
}
|
22
resources/js/components/calendar/constants.js
vendored
Normal file
22
resources/js/components/calendar/constants.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
const DAYS_IN_WEEK = 7;
|
||||
|
||||
const MONTH_LABELS = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
const DAY_LABELS = ['', 'Mon', '', 'Wed', '', 'Fri', ''];
|
||||
|
||||
export { MILLISECONDS_IN_ONE_DAY, DAYS_IN_WEEK, MONTH_LABELS, DAY_LABELS };
|
Reference in New Issue
Block a user