@kkkarsss/ui 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/information/calendar-like/calendar-hour-slot.d.ts +8 -0
- package/dist/ui/information/calendar-like/calendar-hour-slot.js +44 -0
- package/dist/ui/information/calendar-like/calendar-item-wrapper.d.ts +15 -0
- package/dist/ui/information/calendar-like/calendar-item-wrapper.js +58 -0
- package/dist/ui/information/calendar-like/calendar-item.d.ts +13 -0
- package/dist/ui/information/calendar-like/calendar-item.js +14 -0
- package/dist/ui/information/calendar-like/calendar-like.d.ts +5 -23
- package/dist/ui/information/calendar-like/calendar-like.js +20 -67
- package/dist/ui/information/calendar-like/calendar-like.stories.d.ts +6 -0
- package/dist/ui/information/calendar-like/calendar-like.stories.js +91 -0
- package/dist/ui/information/calendar-like/index.d.ts +5 -0
- package/dist/ui/information/calendar-like/index.js +5 -0
- package/dist/ui/information/calendar-like/types.d.ts +24 -0
- package/dist/ui/information/calendar-like/types.js +1 -0
- package/dist/ui/information/calendar-like/utils.d.ts +3 -0
- package/dist/ui/information/calendar-like/utils.js +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FC, type DragEvent, ReactNode } from 'react';
|
|
2
|
+
export type TCalendarHourSlotProps = {
|
|
3
|
+
hour: number;
|
|
4
|
+
onDrop?: (e: DragEvent<HTMLDivElement>, hour: number) => void;
|
|
5
|
+
onCreateTask?: (hour: number, minutes: number, estimatedTime: number) => void;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
export declare const CalendarHourSlot: FC<TCalendarHourSlotProps>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Typo } from '../text/typo';
|
|
4
|
+
export const CalendarHourSlot = ({ hour, onDrop, onCreateTask, children, }) => {
|
|
5
|
+
const [selection, setSelection] = useState(null);
|
|
6
|
+
const handleDragOver = (e) => {
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
};
|
|
9
|
+
const handleMouseDown = (e) => {
|
|
10
|
+
if (e.button !== 0 || !onCreateTask)
|
|
11
|
+
return;
|
|
12
|
+
if (e.target.closest('.calendar-item'))
|
|
13
|
+
return;
|
|
14
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
15
|
+
const offsetY = e.clientY - rect.top;
|
|
16
|
+
const startMinutes = Math.floor(offsetY / 20) * 15;
|
|
17
|
+
setSelection({
|
|
18
|
+
startMinutes,
|
|
19
|
+
currentEstimatedTime: 15,
|
|
20
|
+
});
|
|
21
|
+
const startY = e.clientY;
|
|
22
|
+
const onMouseMove = (moveEvent) => {
|
|
23
|
+
const deltaY = moveEvent.clientY - startY;
|
|
24
|
+
const newEstimatedTime = Math.max(15, Math.round(((deltaY / 80) * 60) / 15) * 15);
|
|
25
|
+
setSelection((prev) => (prev ? { ...prev, currentEstimatedTime: newEstimatedTime } : null));
|
|
26
|
+
};
|
|
27
|
+
const onMouseUp = () => {
|
|
28
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
29
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
30
|
+
setSelection((finalSelection) => {
|
|
31
|
+
if (finalSelection) {
|
|
32
|
+
onCreateTask(hour, finalSelection.startMinutes, finalSelection.currentEstimatedTime);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
38
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs("div", { onDragOver: handleDragOver, onDrop: (e) => onDrop?.(e, hour), onMouseDown: handleMouseDown, className: "relative h-20 border-b border-secondary-foreground group hover:z-10 has-[.calendar-item:hover]:z-30 has-[.calendar-item:active]:z-30", children: [_jsx("div", { className: "absolute -left-12 top-0 -translate-y-1/2 w-10 text-right", children: _jsxs(Typo, { size: "xs", color: "secondary", children: [String(hour).padStart(2, '0'), ":00"] }) }), _jsxs("div", { className: "pl-4 py-1 relative h-full", children: [children, selection && (_jsx("div", { className: "absolute left-4 right-1 bg-accent/30 border border-accent rounded-sm z-[40] pointer-events-none", style: {
|
|
41
|
+
top: `${(selection.startMinutes / 60) * 80}px`,
|
|
42
|
+
height: `${(selection.currentEstimatedTime / 60) * 80}px`,
|
|
43
|
+
}, children: _jsxs("div", { className: "p-1", children: [_jsx(Typo, { size: "xs", weight: "500", children: "\u041D\u043E\u0432\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430" }), _jsxs(Typo, { size: "xs", color: "secondary", children: [selection.currentEstimatedTime, " \u043C\u0438\u043D"] })] }) }))] })] }));
|
|
44
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FC, type ReactNode, type DragEvent } from 'react';
|
|
2
|
+
import { TCalendarTask } from './types';
|
|
3
|
+
export type TCalendarItemWrapperProps = {
|
|
4
|
+
task: TCalendarTask;
|
|
5
|
+
position: {
|
|
6
|
+
width: string;
|
|
7
|
+
left: string;
|
|
8
|
+
top: string;
|
|
9
|
+
height: string;
|
|
10
|
+
};
|
|
11
|
+
onDragStart?: (e: DragEvent<HTMLDivElement>, id: string) => void;
|
|
12
|
+
onResize?: (id: string, newEstimatedTime: number) => void;
|
|
13
|
+
renderTask: (task: TCalendarTask) => ReactNode;
|
|
14
|
+
};
|
|
15
|
+
export declare const CalendarItemWrapper: FC<TCalendarItemWrapperProps>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { jc } from '../../../utils';
|
|
4
|
+
export const CalendarItemWrapper = ({ task, position, onDragStart, onResize, renderTask, }) => {
|
|
5
|
+
const [previewEstimatedTime, setPreviewEstimatedTime] = useState(null);
|
|
6
|
+
const handleMouseDown = (e) => {
|
|
7
|
+
if (onResize) {
|
|
8
|
+
e.preventDefault();
|
|
9
|
+
e.stopPropagation();
|
|
10
|
+
const startY = e.clientY;
|
|
11
|
+
const startHeight = (task.estimatedTime / 60) * 80;
|
|
12
|
+
const onMouseMove = (moveEvent) => {
|
|
13
|
+
const deltaY = moveEvent.clientY - startY;
|
|
14
|
+
const newHeight = Math.max(20, startHeight + deltaY);
|
|
15
|
+
const newEstimatedTime = Math.max(15, Math.round(((newHeight / 80) * 60) / 15) * 15);
|
|
16
|
+
setPreviewEstimatedTime(newEstimatedTime);
|
|
17
|
+
};
|
|
18
|
+
const onMouseUp = (upEvent) => {
|
|
19
|
+
const deltaY = upEvent.clientY - startY;
|
|
20
|
+
const newHeight = Math.max(20, startHeight + deltaY);
|
|
21
|
+
const newEstimatedTime = Math.max(15, Math.round(((newHeight / 80) * 60) / 15) * 15);
|
|
22
|
+
if (newEstimatedTime !== task.estimatedTime) {
|
|
23
|
+
onResize(task.id, newEstimatedTime);
|
|
24
|
+
}
|
|
25
|
+
setPreviewEstimatedTime(null);
|
|
26
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
27
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
28
|
+
const clickPreventer = (e) => {
|
|
29
|
+
e.stopImmediatePropagation();
|
|
30
|
+
document.removeEventListener('click', clickPreventer, true);
|
|
31
|
+
};
|
|
32
|
+
document.addEventListener('click', clickPreventer, true);
|
|
33
|
+
};
|
|
34
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
35
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const displayHeight = previewEstimatedTime
|
|
39
|
+
? `${(previewEstimatedTime / 60) * 80}px`
|
|
40
|
+
: position.height;
|
|
41
|
+
return (_jsx("div", { draggable: !!onDragStart, onDragStart: (e) => {
|
|
42
|
+
onDragStart?.(e, task.id);
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
const el = e.currentTarget;
|
|
45
|
+
if (el)
|
|
46
|
+
el.style.pointerEvents = 'none';
|
|
47
|
+
}, 0);
|
|
48
|
+
}, onDragEnd: (e) => {
|
|
49
|
+
const el = e.currentTarget;
|
|
50
|
+
if (el)
|
|
51
|
+
el.style.pointerEvents = '';
|
|
52
|
+
}, style: {
|
|
53
|
+
height: displayHeight,
|
|
54
|
+
width: position.width,
|
|
55
|
+
left: position.left,
|
|
56
|
+
top: position.top,
|
|
57
|
+
}, className: jc('calendar-item absolute p-[2px] transition-all flex flex-col z-50 select-none', onDragStart ? 'cursor-move' : '', 'hover:z-[60]', previewEstimatedTime !== null ? 'transition-none z-[60]' : ''), children: _jsxs("div", { className: "w-full h-full relative group/item", children: [renderTask(task), onResize && (_jsx("div", { className: "absolute bottom-0 left-0 right-1 h-2 cursor-ns-resize group/resize flex items-center justify-center z-[70]", onMouseDown: handleMouseDown, children: _jsx("div", { className: "w-8 h-1 bg-accent/30 rounded-full opacity-0 group-hover/resize:opacity-100 transition-opacity" }) }))] }) }));
|
|
58
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FC, type ReactNode } from 'react';
|
|
2
|
+
export type TCalendarItemProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
isCompleted?: boolean;
|
|
6
|
+
isInWork?: boolean;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
estimatedTime?: number;
|
|
10
|
+
color?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const CalendarItem: FC<TCalendarItemProps>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExternalLink } from 'lucide-react';
|
|
3
|
+
import { jc } from '../../../utils';
|
|
4
|
+
import { Flex } from '../../layout';
|
|
5
|
+
import { Typo } from '../text/typo';
|
|
6
|
+
export const CalendarItem = ({ title, description, isCompleted, isInWork, onClick, icon, estimatedTime = 30, color, className, }) => {
|
|
7
|
+
return (_jsxs("div", { className: jc('w-full h-full p-1 rounded-sm border flex flex-col transition-colors relative', !color && (isCompleted ? 'opacity-50 border-secondary bg-accent/50' : 'border-accent bg-accent'), color && (isCompleted ? 'opacity-50' : ''), className), style: {
|
|
8
|
+
backgroundColor: color ? `${color}80` : undefined,
|
|
9
|
+
borderColor: color ? color : undefined,
|
|
10
|
+
}, children: [_jsxs(Flex, { gap: "8px", align: "center", justify: "space-between", type: "fill", children: [_jsxs(Flex, { gap: "8px", align: "center", type: "fill", className: "min-w-0 flex-1", children: [_jsx(Typo, { size: "s", weight: "500", textDecoration: isCompleted ? 'line-through' : 'none', className: "truncate", children: title }), isInWork && icon] }), onClick && (_jsx("div", { onClick: (e) => {
|
|
11
|
+
e.stopPropagation();
|
|
12
|
+
onClick();
|
|
13
|
+
}, className: "cursor-pointer hover:text-primary transition-colors p-0.5", children: _jsx(ExternalLink, { size: 14 }) }))] }), description && estimatedTime > 30 && (_jsx("div", { className: "truncate", children: _jsx(Typo, { size: "xs", color: "secondary", children: description }) }))] }));
|
|
14
|
+
};
|
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
import { type FC
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
isCompleted?: boolean;
|
|
7
|
-
isInWork?: boolean;
|
|
8
|
-
onDragStart?: (e: DragEvent<HTMLDivElement>, id: string) => void;
|
|
9
|
-
onClick?: (id: string) => void;
|
|
10
|
-
onResize?: (id: string, newEstimatedTime: number) => void;
|
|
11
|
-
icon?: ReactNode;
|
|
12
|
-
estimatedTime?: number;
|
|
13
|
-
width?: string;
|
|
14
|
-
left?: string;
|
|
15
|
-
className?: string;
|
|
16
|
-
color?: string;
|
|
17
|
-
};
|
|
18
|
-
export declare const CalendarItem: FC<TCalendarItemProps>;
|
|
19
|
-
export type TCalendarLikeProps = {
|
|
20
|
-
hours?: number[];
|
|
21
|
-
onDrop?: (e: DragEvent<HTMLDivElement>, hour: number) => void;
|
|
22
|
-
renderItem?: (hour: number) => ReactNode;
|
|
23
|
-
};
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
import { TCalendarLikeProps } from './types';
|
|
3
|
+
export * from './types';
|
|
4
|
+
export * from './calendar-item-wrapper';
|
|
5
|
+
export * from './calendar-item';
|
|
24
6
|
export declare const CalendarLike: FC<TCalendarLikeProps>;
|
|
@@ -1,71 +1,24 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { CalendarHourSlot } from './calendar-hour-slot';
|
|
4
|
+
import { CalendarItemWrapper } from './calendar-item-wrapper';
|
|
5
|
+
import { calculateTaskPositions, groupTasksByHour } from './utils';
|
|
6
6
|
import { Offset } from '../../layout';
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
const handleMouseDown = (e) => {
|
|
11
|
-
if (onResize) {
|
|
12
|
-
e.preventDefault();
|
|
13
|
-
e.stopPropagation();
|
|
14
|
-
const startY = e.clientY;
|
|
15
|
-
const startHeight = (estimatedTime / 60) * 80;
|
|
16
|
-
const onMouseMove = (moveEvent) => {
|
|
17
|
-
const deltaY = moveEvent.clientY - startY;
|
|
18
|
-
const newHeight = Math.max(20, startHeight + deltaY);
|
|
19
|
-
// Round to nearest 15 minutes
|
|
20
|
-
const newEstimatedTime = Math.max(15, Math.round(((newHeight / 80) * 60) / 15) * 15);
|
|
21
|
-
setPreviewEstimatedTime(newEstimatedTime);
|
|
22
|
-
};
|
|
23
|
-
const onMouseUp = (upEvent) => {
|
|
24
|
-
const deltaY = upEvent.clientY - startY;
|
|
25
|
-
const newHeight = Math.max(20, startHeight + deltaY);
|
|
26
|
-
const newEstimatedTime = Math.max(15, Math.round(((newHeight / 80) * 60) / 15) * 15);
|
|
27
|
-
if (newEstimatedTime !== estimatedTime) {
|
|
28
|
-
onResize(id, newEstimatedTime);
|
|
29
|
-
}
|
|
30
|
-
setPreviewEstimatedTime(null);
|
|
31
|
-
document.removeEventListener('mousemove', onMouseMove);
|
|
32
|
-
document.removeEventListener('mouseup', onMouseUp);
|
|
33
|
-
// Prevent click event after resize
|
|
34
|
-
const clickPreventer = (e) => {
|
|
35
|
-
e.stopImmediatePropagation();
|
|
36
|
-
document.removeEventListener('click', clickPreventer, true);
|
|
37
|
-
};
|
|
38
|
-
document.addEventListener('click', clickPreventer, true);
|
|
39
|
-
};
|
|
40
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
41
|
-
document.addEventListener('mouseup', onMouseUp);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
const displayEstimatedTime = previewEstimatedTime ?? estimatedTime;
|
|
45
|
-
return (_jsx("div", { draggable: !!onDragStart, onDragStart: (e) => {
|
|
46
|
-
onDragStart?.(e, id);
|
|
47
|
-
// Delay applying pointer-events-none to allow the drag operation to start
|
|
48
|
-
setTimeout(() => {
|
|
49
|
-
const el = e.currentTarget;
|
|
50
|
-
if (el)
|
|
51
|
-
el.style.pointerEvents = 'none';
|
|
52
|
-
}, 0);
|
|
53
|
-
}, onDragEnd: (e) => {
|
|
54
|
-
const el = e.currentTarget;
|
|
55
|
-
if (el)
|
|
56
|
-
el.style.pointerEvents = '';
|
|
57
|
-
}, style: { height: `${(displayEstimatedTime / 60) * 80}px`, width, left }, className: jc('calendar-item absolute p-[2px] transition-all flex flex-col z-50 select-none', onDragStart ? 'cursor-move' : '', 'hover:z-[60]', previewEstimatedTime !== null ? 'transition-none z-[60]' : '', className), children: _jsxs("div", { className: jc('w-full h-full p-1 rounded-sm border flex flex-col transition-colors relative', !color && (isCompleted ? 'opacity-50 border-secondary bg-accent/50' : 'border-accent bg-accent'), color && (isCompleted ? 'opacity-50' : ''), previewEstimatedTime !== null ? '!opacity-100' : ''), style: {
|
|
58
|
-
backgroundColor: color ? `${color}80` : undefined,
|
|
59
|
-
borderColor: color ? color : undefined,
|
|
60
|
-
}, children: [_jsxs(Flex, { gap: "8px", align: "center", justify: "space-between", type: "fill", children: [_jsxs(Flex, { gap: "8px", align: "center", type: "fill", className: "min-w-0 flex-1", children: [_jsx(Typo, { size: "s", weight: "500", textDecoration: isCompleted ? 'line-through' : 'none', className: "truncate", children: title }), isInWork && icon] }), onClick && (_jsx("div", { onClick: (e) => {
|
|
61
|
-
e.stopPropagation();
|
|
62
|
-
onClick(id);
|
|
63
|
-
}, className: "cursor-pointer hover:text-primary transition-colors p-0.5", children: _jsx(ExternalLink, { size: 14 }) }))] }), description && displayEstimatedTime > 30 && (_jsx("div", { className: "truncate", children: _jsx(Typo, { size: "xs", color: "secondary", children: description }) })), onResize && !isCompleted && (_jsx("div", { className: "absolute bottom-0 left-0 right-1 h-2 cursor-ns-resize group/resize flex items-center justify-center", onMouseDown: handleMouseDown, children: _jsx("div", { className: "w-8 h-1 bg-accent/30 rounded-full opacity-0 group-hover/resize:opacity-100 transition-opacity" }) }))] }) }));
|
|
64
|
-
};
|
|
7
|
+
export * from './types';
|
|
8
|
+
export * from './calendar-item-wrapper';
|
|
9
|
+
export * from './calendar-item';
|
|
65
10
|
const DEFAULT_HOURS = Array.from({ length: 24 }, (_, i) => i);
|
|
66
|
-
export const CalendarLike = ({ hours = DEFAULT_HOURS,
|
|
67
|
-
const
|
|
68
|
-
|
|
11
|
+
export const CalendarLike = ({ tasks, hours = DEFAULT_HOURS, renderTask, onTaskDrop, onTaskResize, onCreateTask, }) => {
|
|
12
|
+
const tasksWithPosition = useMemo(() => calculateTaskPositions(tasks), [tasks]);
|
|
13
|
+
const tasksByHour = useMemo(() => groupTasksByHour(tasksWithPosition), [tasksWithPosition]);
|
|
14
|
+
const handleDragStart = (e, taskId) => {
|
|
15
|
+
e.dataTransfer.setData('taskId', taskId);
|
|
16
|
+
};
|
|
17
|
+
const handleDrop = (e, hour) => {
|
|
18
|
+
const taskId = e.dataTransfer.getData('taskId');
|
|
19
|
+
if (taskId && onTaskDrop) {
|
|
20
|
+
onTaskDrop(taskId, hour);
|
|
21
|
+
}
|
|
69
22
|
};
|
|
70
|
-
return (_jsx(Offset, { type: 'vertical', children: _jsx("div", { className: "relative border-l border-secondary/20 ml-12", children: hours.map((hour) => (
|
|
23
|
+
return (_jsx(Offset, { type: 'vertical', children: _jsx("div", { className: "relative border-l border-secondary/20 ml-12 select-none", children: hours.map((hour) => (_jsx(CalendarHourSlot, { hour: hour, onDrop: handleDrop, onCreateTask: onCreateTask, children: tasksByHour[hour]?.map((task) => (_jsx(CalendarItemWrapper, { task: task, position: task.position, onDragStart: handleDragStart, onResize: onTaskResize, renderTask: renderTask }, task.id))) }, hour))) }) }));
|
|
71
24
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Clock } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { CalendarLike, CalendarItem } from './calendar-like';
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Information/CalendarLike',
|
|
7
|
+
component: CalendarLike,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
const getTodayAtHour = (hour, minutes = 0) => {
|
|
12
|
+
const date = new Date();
|
|
13
|
+
date.setHours(hour, minutes, 0, 0);
|
|
14
|
+
return date;
|
|
15
|
+
};
|
|
16
|
+
const INITIAL_TASKS = [
|
|
17
|
+
{
|
|
18
|
+
id: '1',
|
|
19
|
+
title: 'Утренняя почта',
|
|
20
|
+
description: 'Проверить входящие и ответить на важные письма',
|
|
21
|
+
dueDate: getTodayAtHour(9),
|
|
22
|
+
estimatedTime: 45,
|
|
23
|
+
color: '#3b82f6',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: '2',
|
|
27
|
+
title: 'Стендап',
|
|
28
|
+
dueDate: getTodayAtHour(10),
|
|
29
|
+
estimatedTime: 15,
|
|
30
|
+
color: '#10b981',
|
|
31
|
+
isInWork: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: '3',
|
|
35
|
+
title: 'Обед',
|
|
36
|
+
dueDate: getTodayAtHour(13),
|
|
37
|
+
estimatedTime: 60,
|
|
38
|
+
color: '#f59e0b',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: '4',
|
|
42
|
+
title: 'Разработка фичи',
|
|
43
|
+
description: 'Реализация логики календаря',
|
|
44
|
+
dueDate: getTodayAtHour(14),
|
|
45
|
+
estimatedTime: 120,
|
|
46
|
+
color: '#8b5cf6',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: '5',
|
|
50
|
+
title: 'Завершенная задача',
|
|
51
|
+
dueDate: getTodayAtHour(17),
|
|
52
|
+
estimatedTime: 30,
|
|
53
|
+
isCompleted: true,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
export const Interactive = {
|
|
57
|
+
render: () => {
|
|
58
|
+
const [tasks, setTasks] = useState(INITIAL_TASKS);
|
|
59
|
+
const handleTaskDrop = (taskId, hour) => {
|
|
60
|
+
console.log(`Task ${taskId} dropped at hour ${hour}`);
|
|
61
|
+
setTasks((prev) => prev.map((t) => {
|
|
62
|
+
if (t.id === taskId) {
|
|
63
|
+
const newDate = new Date(t.dueDate);
|
|
64
|
+
newDate.setHours(hour, 0, 0, 0);
|
|
65
|
+
return { ...t, dueDate: newDate };
|
|
66
|
+
}
|
|
67
|
+
return t;
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
70
|
+
const handleTaskResize = (taskId, newEstimatedTime) => {
|
|
71
|
+
console.log(`Task ${taskId} resized to ${newEstimatedTime} minutes`);
|
|
72
|
+
setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, estimatedTime: newEstimatedTime } : t)));
|
|
73
|
+
};
|
|
74
|
+
const handleCreateTask = (hour, minutes, estimatedTime) => {
|
|
75
|
+
console.log(`Create task at ${hour}:${minutes} with duration ${estimatedTime}`);
|
|
76
|
+
const newTask = {
|
|
77
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
78
|
+
title: 'Новая задача',
|
|
79
|
+
dueDate: getTodayAtHour(hour, minutes),
|
|
80
|
+
estimatedTime,
|
|
81
|
+
color: '#8b5cf6',
|
|
82
|
+
};
|
|
83
|
+
setTasks((prev) => [...prev, newTask]);
|
|
84
|
+
};
|
|
85
|
+
const handleTaskClick = (taskId) => {
|
|
86
|
+
console.log(`Task ${taskId} clicked`);
|
|
87
|
+
alert(`Клик по задаче ${taskId}`);
|
|
88
|
+
};
|
|
89
|
+
return (_jsxs("div", { style: { height: '600px', overflowY: 'auto', background: 'var(--bg-primary)', padding: '20px' }, children: [_jsxs("p", { style: { marginBottom: '20px', color: 'var(--secondary)' }, children: ["\u0418\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044F: ", _jsx("br", {}), "1. \u041F\u0435\u0440\u0435\u0442\u0430\u0441\u043A\u0438\u0432\u0430\u0439\u0442\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u043C\u0435\u0436\u0434\u0443 \u0447\u0430\u0441\u0430\u043C\u0438. ", _jsx("br", {}), "2. \u0418\u0437\u043C\u0435\u043D\u044F\u0439\u0442\u0435 \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0438, \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u0437\u0430 \u043D\u0438\u0436\u043D\u0438\u0439 \u043A\u0440\u0430\u0439. ", _jsx("br", {}), "3. \u0421\u043E\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438, \u043A\u043B\u0438\u043A\u043D\u0443\u0432 \u0438 \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u043D\u0430 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u043E\u043C \u043C\u0435\u0441\u0442\u0435. ", _jsx("br", {}), "\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0432\u044B\u0432\u043E\u0434\u044F\u0442\u0441\u044F \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u044C."] }), _jsx(CalendarLike, { tasks: tasks, onTaskDrop: handleTaskDrop, onTaskResize: handleTaskResize, onCreateTask: handleCreateTask, renderTask: (task) => (_jsx(CalendarItem, { title: task.title, description: task.description, color: task.color, isCompleted: task.isCompleted, isInWork: task.isInWork, estimatedTime: task.estimatedTime, icon: task.isInWork ? _jsx(Clock, { size: 12, className: "text-white animate-pulse" }) : undefined, onClick: () => handleTaskClick(task.id) })) })] }));
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export type TCalendarTask = {
|
|
3
|
+
id: string;
|
|
4
|
+
dueDate: Date;
|
|
5
|
+
estimatedTime: number;
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
};
|
|
8
|
+
export type TCalendarTaskWithPosition = TCalendarTask & {
|
|
9
|
+
position: {
|
|
10
|
+
width: string;
|
|
11
|
+
left: string;
|
|
12
|
+
top: string;
|
|
13
|
+
height: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type TCalendarLikeProps = {
|
|
17
|
+
tasks: TCalendarTask[];
|
|
18
|
+
hours?: number[];
|
|
19
|
+
renderTask: (task: TCalendarTask) => ReactNode;
|
|
20
|
+
onTaskDrop?: (taskId: string, hour: number) => void;
|
|
21
|
+
onTaskResize?: (taskId: string, newEstimatedTime: number) => void;
|
|
22
|
+
onTaskClick?: (taskId: string) => void;
|
|
23
|
+
onCreateTask?: (hour: number, minutes: number, estimatedTime: number) => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { TCalendarTask, TCalendarTaskWithPosition } from './types';
|
|
2
|
+
export declare const calculateTaskPositions: (tasks: TCalendarTask[]) => TCalendarTaskWithPosition[];
|
|
3
|
+
export declare const groupTasksByHour: (tasksWithPosition: TCalendarTaskWithPosition[]) => Record<number, TCalendarTaskWithPosition[]>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const calculateTaskPositions = (tasks) => {
|
|
2
|
+
const tasksToRender = [...tasks].filter((t) => t.dueDate).sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime());
|
|
3
|
+
const columns = [];
|
|
4
|
+
tasksToRender.forEach((task) => {
|
|
5
|
+
const startTime = task.dueDate.getTime();
|
|
6
|
+
let placed = false;
|
|
7
|
+
for (const column of columns) {
|
|
8
|
+
const lastTask = column[column.length - 1];
|
|
9
|
+
const lastEndTime = lastTask.dueDate.getTime() + lastTask.estimatedTime * 60 * 1000;
|
|
10
|
+
if (startTime >= lastEndTime) {
|
|
11
|
+
column.push(task);
|
|
12
|
+
placed = true;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (!placed) {
|
|
17
|
+
columns.push([task]);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const clusters = [];
|
|
21
|
+
let currentCluster = [];
|
|
22
|
+
let clusterMaxEndTime = 0;
|
|
23
|
+
columns.forEach((column) => {
|
|
24
|
+
let columnMinStartTime = Infinity;
|
|
25
|
+
column.forEach((t) => {
|
|
26
|
+
const start = t.dueDate.getTime();
|
|
27
|
+
if (start < columnMinStartTime)
|
|
28
|
+
columnMinStartTime = start;
|
|
29
|
+
});
|
|
30
|
+
if (columnMinStartTime >= clusterMaxEndTime && currentCluster.length > 0) {
|
|
31
|
+
clusters.push(currentCluster);
|
|
32
|
+
currentCluster = [];
|
|
33
|
+
clusterMaxEndTime = 0;
|
|
34
|
+
}
|
|
35
|
+
currentCluster.push(column);
|
|
36
|
+
column.forEach((t) => {
|
|
37
|
+
const end = t.dueDate.getTime() + t.estimatedTime * 60 * 1000;
|
|
38
|
+
if (end > clusterMaxEndTime)
|
|
39
|
+
clusterMaxEndTime = end;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
if (currentCluster.length > 0)
|
|
43
|
+
clusters.push(currentCluster);
|
|
44
|
+
const result = [];
|
|
45
|
+
clusters.forEach((cluster) => {
|
|
46
|
+
const clusterColumnsCount = cluster.length;
|
|
47
|
+
cluster.forEach((column, colIndex) => {
|
|
48
|
+
column.forEach((task) => {
|
|
49
|
+
const width = clusterColumnsCount > 1 ? `${100 / clusterColumnsCount}%` : '100%';
|
|
50
|
+
const left = clusterColumnsCount > 1 ? `${(colIndex * 100) / clusterColumnsCount}%` : '0%';
|
|
51
|
+
const top = `${(task.dueDate.getMinutes() / 60) * 80}px`;
|
|
52
|
+
const height = `${(task.estimatedTime / 60) * 80}px`;
|
|
53
|
+
result.push({
|
|
54
|
+
...task,
|
|
55
|
+
position: { width, left, top, height },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return result;
|
|
61
|
+
};
|
|
62
|
+
export const groupTasksByHour = (tasksWithPosition) => {
|
|
63
|
+
const map = {};
|
|
64
|
+
tasksWithPosition.forEach((task) => {
|
|
65
|
+
const hour = task.dueDate.getHours();
|
|
66
|
+
if (!map[hour])
|
|
67
|
+
map[hour] = [];
|
|
68
|
+
map[hour].push(task);
|
|
69
|
+
});
|
|
70
|
+
return map;
|
|
71
|
+
};
|