🚧 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 };
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user