🚧 Commence l'ajout de calendrier

This commit is contained in:
2022-03-21 16:53:22 +01:00
parent e1f2294fe4
commit d7442355dc
12 changed files with 1098 additions and 25 deletions

View 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;

View 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
}

View File

@@ -0,0 +1,5 @@
export interface CalendarHeatmapValuesProp {
id: string;
date: Date;
event: string;
}

View 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 };

View File

@@ -14,6 +14,8 @@ import {
} from "react-router-dom";
import {IList} from "../../interfaces/IList";
import {isAllLoadedLocally} from "../../utils";
import CalendarHeatmap from "../calendar/CalendarHeatmap";
import {CalendarHeatmapValuesProp} from "../calendar/CalendarHeatmapValuesProp";
const app = document.getElementById('app');
const word = "shikiryu"; // FIXME should be in db and ≠ between users
@@ -24,12 +26,14 @@ let getPageContentUrl,
postUrl,
removeUrl,
checkword,
startDate,
csrf = "";
if (app) {
getPageContentUrl = "" + app.getAttribute('data-url');
pages = JSON.parse("" + app.getAttribute('data-list')) as IList[];
postUrl = "" + app.getAttribute('data-post');
startDate = "" + app.getAttribute('data-start');
removeUrl = "" + app.getAttribute('data-remove');
csrf = "" + app.getAttribute('data-csrf');
checkword = "" + app.getAttribute('data-checkword');
@@ -157,9 +161,33 @@ function App() {
return (<Prompt open={true} setOpen={updatePassphrase}/>);
}
function getCalendarValuesFromPagesList(pages: IList[]): CalendarHeatmapValuesProp[] {
return pages.map(page => {
const [date, ] = page.date.split(" ");
const splittedDate = date.split("/").map(part => { return parseInt(part, 10);});
return {
id: page.id,
date: new Date(splittedDate[2] as number, splittedDate[1] - 1, splittedDate[0] as number),
event: page.id
};
});
}
function ListPage() {
return (
<div className="col-md-12">
<CalendarHeatmap
values={getCalendarValuesFromPagesList(listPages)}
startDate={startDate}
endDate={new Date()}
showMonthLabels={true}
showWeekdayLabels={true}
showOutOfRangeDays={true}
horizontal={true}
gutterSize={4}
onClick={(value) => alert(value)}
tooltipDataAttrs={{}}
/>
<Pages
pages={listPages}
url={getPageContentUrl}

View File

@@ -6,7 +6,7 @@ let encryptStorage = new EncryptStorage('test'); // TODO la clef doit venir de l
export default function PageForm({setListPages, csrf, url, passphrase}) {
const isPassphraseSet = passphrase !== null;
const [content, setContent] = React.useState("");
const [content, setContent] = React.useState<string>("");
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
encryptStorage = new EncryptStorage(passphrase);
@@ -56,6 +56,9 @@ export default function PageForm({setListPages, csrf, url, passphrase}) {
]);
}
}
function updateContent(value: string|undefined): void {
setContent(value as string);
}
if (isPassphraseSet) {
return (
@@ -73,7 +76,7 @@ export default function PageForm({setListPages, csrf, url, passphrase}) {
/>
<MDEditor
value={content}
onChange={setContent}
onChange={updateContent}
/>
<Button variant="contained" type={"submit"}>
Enregistrer