@kkkarsss/ui 1.4.9 → 1.4.11
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-item-wrapper.d.ts +1 -1
- package/dist/ui/information/calendar-like/calendar-item-wrapper.js +56 -16
- package/dist/ui/information/calendar-like/calendar-like.js +13 -77
- package/dist/ui/information/calendar-like/calendar-like.stories.js +15 -4
- package/dist/ui/information/calendar-like/calendar-slot.d.ts +1 -0
- package/dist/ui/information/calendar-like/calendar-slot.js +1 -4
- package/dist/ui/information/calendar-like/types.d.ts +1 -1
- package/dist/ui/information/calendar-like/use-auto-scroll.d.ts +2 -0
- package/dist/ui/information/calendar-like/use-auto-scroll.js +49 -0
- package/dist/ui/information/calendar-like/use-current-time.d.ts +4 -0
- package/dist/ui/information/calendar-like/use-current-time.js +18 -0
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ export type TCalendarItemWrapperProps = {
|
|
|
10
10
|
};
|
|
11
11
|
onDragStart?: (e: DragEvent<HTMLDivElement>, id: string) => void;
|
|
12
12
|
onDragEnd?: (e: DragEvent<HTMLDivElement>, id: string) => void;
|
|
13
|
-
onResize?: (id: string, newEstimatedTime: number) => void;
|
|
13
|
+
onResize?: (id: string, newEstimatedTime: number, newStartHour?: number, newStartMinutes?: number) => void;
|
|
14
14
|
renderTask: (task: TCalendarTask) => ReactNode;
|
|
15
15
|
overlappingTasksCount?: number;
|
|
16
16
|
};
|
|
@@ -2,28 +2,60 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { jc } from '../../../utils';
|
|
4
4
|
export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, onResize, renderTask, }) => {
|
|
5
|
-
const [
|
|
6
|
-
const
|
|
5
|
+
const [preview, setPreview] = useState(null);
|
|
6
|
+
const handleResize = (e, type) => {
|
|
7
7
|
if (onResize) {
|
|
8
8
|
e.preventDefault();
|
|
9
9
|
e.stopPropagation();
|
|
10
10
|
const startY = e.clientY;
|
|
11
11
|
const startHeight = (task.estimatedTime / 15) * 20;
|
|
12
|
+
const startHour = task.dueDate.getHours();
|
|
13
|
+
const startMinutes = task.dueDate.getMinutes();
|
|
12
14
|
const onMouseMove = (moveEvent) => {
|
|
13
15
|
const deltaY = moveEvent.clientY - startY;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
let newHeight;
|
|
17
|
+
let newEstimatedTime;
|
|
18
|
+
let newStartHour = startHour;
|
|
19
|
+
let newStartMinutes = startMinutes;
|
|
20
|
+
if (type === 'bottom') {
|
|
21
|
+
newHeight = Math.max(20, startHeight + deltaY);
|
|
22
|
+
newEstimatedTime = Math.max(15, Math.round(newHeight / 20) * 15);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Resizing top
|
|
26
|
+
const deltaMinutes = Math.round(deltaY / 20) * 15;
|
|
27
|
+
newEstimatedTime = Math.max(15, task.estimatedTime - deltaMinutes);
|
|
28
|
+
// If we hit min height, don't move the top further down
|
|
29
|
+
const actualDeltaMinutes = task.estimatedTime - newEstimatedTime;
|
|
30
|
+
const totalStartMinutes = startHour * 60 + startMinutes + actualDeltaMinutes;
|
|
31
|
+
newStartHour = Math.floor(totalStartMinutes / 60);
|
|
32
|
+
newStartMinutes = totalStartMinutes % 60;
|
|
33
|
+
}
|
|
34
|
+
setPreview({ estimatedTime: newEstimatedTime, startHour: newStartHour, startMinutes: newStartMinutes });
|
|
18
35
|
};
|
|
19
36
|
const onMouseUp = (upEvent) => {
|
|
20
37
|
const deltaY = upEvent.clientY - startY;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
let finalEstimatedTime;
|
|
39
|
+
let finalStartHour = startHour;
|
|
40
|
+
let finalStartMinutes = startMinutes;
|
|
41
|
+
if (type === 'bottom') {
|
|
42
|
+
const finalHeight = Math.max(20, startHeight + deltaY);
|
|
43
|
+
finalEstimatedTime = Math.max(15, Math.round(finalHeight / 20) * 15);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const deltaMinutes = Math.round(deltaY / 20) * 15;
|
|
47
|
+
finalEstimatedTime = Math.max(15, task.estimatedTime - deltaMinutes);
|
|
48
|
+
const actualDeltaMinutes = task.estimatedTime - finalEstimatedTime;
|
|
49
|
+
const totalStartMinutes = startHour * 60 + startMinutes + actualDeltaMinutes;
|
|
50
|
+
finalStartHour = Math.floor(totalStartMinutes / 60);
|
|
51
|
+
finalStartMinutes = totalStartMinutes % 60;
|
|
52
|
+
}
|
|
53
|
+
if (finalEstimatedTime !== task.estimatedTime ||
|
|
54
|
+
finalStartHour !== startHour ||
|
|
55
|
+
finalStartMinutes !== startMinutes) {
|
|
56
|
+
onResize(task.id, finalEstimatedTime, finalStartHour, finalStartMinutes);
|
|
25
57
|
}
|
|
26
|
-
|
|
58
|
+
setPreview(null);
|
|
27
59
|
document.removeEventListener('mousemove', onMouseMove);
|
|
28
60
|
document.removeEventListener('mouseup', onMouseUp);
|
|
29
61
|
const clickPreventer = (e) => {
|
|
@@ -36,9 +68,18 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
36
68
|
document.addEventListener('mouseup', onMouseUp);
|
|
37
69
|
}
|
|
38
70
|
};
|
|
39
|
-
const displayHeight =
|
|
71
|
+
const displayHeight = preview ? `${(preview.estimatedTime / 15) * 20}px` : position.height;
|
|
72
|
+
let displayTop = position.top;
|
|
73
|
+
if (preview && (preview.startHour !== undefined || preview.startMinutes !== undefined)) {
|
|
74
|
+
const originalStartTotalMinutes = task.dueDate.getHours() * 60 + task.dueDate.getMinutes();
|
|
75
|
+
const newStartTotalMinutes = preview.startHour * 60 + preview.startMinutes;
|
|
76
|
+
const diffMinutes = newStartTotalMinutes - originalStartTotalMinutes;
|
|
77
|
+
const diffPixels = (diffMinutes / 15) * 20;
|
|
78
|
+
// Исходный position.top уже включает minutes % 15
|
|
79
|
+
const originalTopPx = parseInt(position.top, 10);
|
|
80
|
+
displayTop = `${originalTopPx + diffPixels}px`;
|
|
81
|
+
}
|
|
40
82
|
return (_jsx("div", { draggable: !!onDragStart, onDragStart: (e) => {
|
|
41
|
-
console.log('CalendarItemWrapper: onDragStart', task.id);
|
|
42
83
|
onDragStart?.(e, task.id);
|
|
43
84
|
const el = e.currentTarget;
|
|
44
85
|
// Создаем пустой элемент для скрытия стандартного ghost image браузера
|
|
@@ -53,7 +94,6 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
53
94
|
}, 0);
|
|
54
95
|
}
|
|
55
96
|
}, onDragEnd: (e) => {
|
|
56
|
-
console.log('CalendarItemWrapper: onDragEnd');
|
|
57
97
|
onDragEnd?.(e, task.id);
|
|
58
98
|
const el = e.currentTarget;
|
|
59
99
|
if (el) {
|
|
@@ -64,6 +104,6 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
64
104
|
height: displayHeight,
|
|
65
105
|
width: position.width,
|
|
66
106
|
left: position.left,
|
|
67
|
-
top:
|
|
68
|
-
}, className: jc('calendar-item absolute p-[2px] transition-[height,width,left,opacity] flex flex-col z-50 select-none', onDragStart ? 'cursor-move' : '', 'hover:z-[60]',
|
|
107
|
+
top: displayTop,
|
|
108
|
+
}, className: jc('calendar-item absolute p-[2px] transition-[height,width,left,top,opacity] flex flex-col z-50 select-none', onDragStart ? 'cursor-move' : '', 'hover:z-[60]', preview !== null ? 'transition-none z-[60]' : '', 'active:z-[100]'), children: _jsxs("div", { className: "w-full h-full relative group/item", children: [onResize && (_jsx("div", { className: "absolute top-0 left-0 right-1 h-2 cursor-ns-resize group/resize flex items-center justify-center z-[70]", onMouseDown: (e) => handleResize(e, 'top'), children: _jsx("div", { className: "w-8 h-1 bg-accent/30 rounded-full opacity-0 group-hover/resize:opacity-100 transition-opacity" }) })), 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: (e) => handleResize(e, 'bottom'), children: _jsx("div", { className: "w-8 h-1 bg-accent/30 rounded-full opacity-0 group-hover/resize:opacity-100 transition-opacity" }) }))] }) }));
|
|
69
109
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState,
|
|
2
|
+
import { useMemo, useState, useRef } from 'react';
|
|
3
3
|
import { CalendarItemWrapper } from './calendar-item-wrapper';
|
|
4
4
|
import { CalendarSlot } from './calendar-slot';
|
|
5
|
+
import { useAutoScroll } from './use-auto-scroll';
|
|
6
|
+
import { useCurrentTime } from './use-current-time';
|
|
5
7
|
import { calculateTaskPositions, groupTasksBySlot } from './utils';
|
|
6
8
|
import { jc } from '../../../utils';
|
|
7
9
|
export * from './types';
|
|
@@ -9,106 +11,40 @@ export * from './calendar-item-wrapper';
|
|
|
9
11
|
export * from './calendar-item';
|
|
10
12
|
const DEFAULT_SLOTS = Array.from({ length: 96 }, (_, i) => i);
|
|
11
13
|
const CurrentTimeLine = () => {
|
|
12
|
-
const
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const interval = setInterval(() => {
|
|
15
|
-
setNow(new Date());
|
|
16
|
-
}, 60000);
|
|
17
|
-
return () => clearInterval(interval);
|
|
18
|
-
}, []);
|
|
19
|
-
const topOffset = useMemo(() => {
|
|
20
|
-
const hours = now.getHours();
|
|
21
|
-
const minutes = now.getMinutes();
|
|
22
|
-
const totalMinutes = hours * 60 + minutes;
|
|
23
|
-
// 15 минут = 20 пикселей (высота слота)
|
|
24
|
-
return totalMinutes * (20 / 15);
|
|
25
|
-
}, [now]);
|
|
14
|
+
const { topOffset } = useCurrentTime();
|
|
26
15
|
return (_jsxs("div", { className: "absolute left-0 right-0 z-[101] pointer-events-none flex items-center", style: { top: `${topOffset}px` }, children: [_jsx("div", { className: "w-2 h-2 rounded-full bg-red-500 -ml-1" }), _jsx("div", { className: "flex-1 h-[2px] bg-red-500" })] }));
|
|
27
16
|
};
|
|
28
17
|
export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = true, autoScrollToCurrentTime = true, renderTask, onTaskDrop, onTaskResize, onCreateTask, }) => {
|
|
29
18
|
const containerRef = useRef(null);
|
|
30
|
-
const [
|
|
19
|
+
const [draggingTask, setDraggingTask] = useState(null);
|
|
31
20
|
const tasksWithPosition = useMemo(() => calculateTaskPositions(tasks), [tasks]);
|
|
32
21
|
const tasksBySlot = useMemo(() => groupTasksBySlot(tasksWithPosition), [tasksWithPosition]);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const scroll = () => {
|
|
36
|
-
if (!containerRef.current)
|
|
37
|
-
return;
|
|
38
|
-
const now = new Date();
|
|
39
|
-
const hours = now.getHours();
|
|
40
|
-
const minutes = now.getMinutes();
|
|
41
|
-
const totalMinutes = hours * 60 + minutes;
|
|
42
|
-
// 15 минут = 20 пикселей (высота слота)
|
|
43
|
-
const topOffset = totalMinutes * (20 / 15);
|
|
44
|
-
// Находим ближайший скроллируемый родитель или используем окно
|
|
45
|
-
const scrollParent = (node) => {
|
|
46
|
-
if (!node)
|
|
47
|
-
return null;
|
|
48
|
-
const style = window.getComputedStyle(node);
|
|
49
|
-
if (/(auto|scroll)/.test(style.overflow + style.overflowY))
|
|
50
|
-
return node;
|
|
51
|
-
return scrollParent(node.parentElement);
|
|
52
|
-
};
|
|
53
|
-
const parent = scrollParent(containerRef.current);
|
|
54
|
-
if (parent) {
|
|
55
|
-
const containerRect = containerRef.current.getBoundingClientRect();
|
|
56
|
-
const parentRect = parent.getBoundingClientRect();
|
|
57
|
-
const relativeTop = containerRect.top - parentRect.top + parent.scrollTop;
|
|
58
|
-
parent.scrollTo({
|
|
59
|
-
top: relativeTop + topOffset - parent.clientHeight / 2,
|
|
60
|
-
behavior: 'smooth',
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const timeoutId = setTimeout(scroll, 100);
|
|
65
|
-
return () => clearTimeout(timeoutId);
|
|
66
|
-
}
|
|
67
|
-
}, [showCurrentTime, autoScrollToCurrentTime]);
|
|
22
|
+
const { topOffset } = useCurrentTime();
|
|
23
|
+
useAutoScroll(containerRef, showCurrentTime && autoScrollToCurrentTime, topOffset);
|
|
68
24
|
const handleDragStart = (e, task) => {
|
|
69
|
-
|
|
70
|
-
setIsDragging(true);
|
|
25
|
+
setDraggingTask(task);
|
|
71
26
|
e.dataTransfer.setData('taskId', task.id);
|
|
72
27
|
e.dataTransfer.setData('taskTitle', task.title || '');
|
|
73
28
|
e.dataTransfer.setData('taskEstimatedTime', String(task.estimatedTime));
|
|
74
29
|
if (task.color) {
|
|
75
30
|
e.dataTransfer.setData('taskColor', task.color);
|
|
76
31
|
}
|
|
77
|
-
// Сохраняем в глобальную переменную для доступа во время dragOver
|
|
78
|
-
// eslint-disable-next-line react-hooks/immutability
|
|
79
|
-
window._draggingTask = task;
|
|
80
32
|
};
|
|
81
33
|
const handleDragEnd = () => {
|
|
82
|
-
|
|
83
|
-
delete window._draggingTask;
|
|
34
|
+
setDraggingTask(null);
|
|
84
35
|
};
|
|
85
36
|
const handleDrop = (e, hour, minutes) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Пытаемся получить taskId из dataTransfer
|
|
89
|
-
let taskId = e.dataTransfer.getData('taskId');
|
|
90
|
-
// Если в dataTransfer пусто (бывает в некоторых браузерах/ситуациях),
|
|
91
|
-
// берем из глобальной переменной, которую мы засетили в handleDragStart
|
|
92
|
-
if (!taskId && window._draggingTask) {
|
|
93
|
-
taskId = window._draggingTask.id;
|
|
94
|
-
console.log('CalendarLike: taskId recovered from _draggingTask:', taskId);
|
|
95
|
-
}
|
|
96
|
-
console.log('CalendarLike: taskId for drop:', taskId);
|
|
37
|
+
const taskId = e.dataTransfer.getData('taskId') || draggingTask?.id;
|
|
38
|
+
setDraggingTask(null);
|
|
97
39
|
if (taskId && onTaskDrop) {
|
|
98
40
|
onTaskDrop(taskId, hour, minutes);
|
|
99
41
|
}
|
|
100
|
-
else {
|
|
101
|
-
console.warn('CalendarLike: taskId or onTaskDrop missing', { taskId, hasOnTaskDrop: !!onTaskDrop });
|
|
102
|
-
}
|
|
103
|
-
// Очищаем глобальную переменную после дропа
|
|
104
|
-
delete window._draggingTask;
|
|
105
42
|
};
|
|
106
|
-
return (_jsxs("div", { ref: containerRef, className: jc('relative border-l border-secondary ml-12 select-none',
|
|
43
|
+
return (_jsxs("div", { ref: containerRef, className: jc('relative border-l border-secondary ml-12 select-none', draggingTask ? 'is-dragging' : ''), children: [showCurrentTime && _jsx(CurrentTimeLine, {}), slots.map((slotIndex) => {
|
|
107
44
|
const tasksForSlot = tasksBySlot[slotIndex];
|
|
108
45
|
const currentSlotTime = slotIndex * 15; // в минутах от начала дня
|
|
109
46
|
// Считаем сколько задач пересекают этот слот (для кластеризации превью)
|
|
110
47
|
// Исключаем текущую перетаскиваемую задачу из подсчета, чтобы превью не считало себя помехой
|
|
111
|
-
const draggingTask = window._draggingTask;
|
|
112
48
|
const overlappingTasksCount = tasks.filter((task) => {
|
|
113
49
|
if (draggingTask && task.id === draggingTask.id)
|
|
114
50
|
return false;
|
|
@@ -116,6 +52,6 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = t
|
|
|
116
52
|
const taskEnd = taskStart + task.estimatedTime;
|
|
117
53
|
return currentSlotTime >= taskStart && currentSlotTime < taskEnd;
|
|
118
54
|
}).length;
|
|
119
|
-
return (_jsx(CalendarSlot, { slotIndex: slotIndex, onDrop: handleDrop, onCreateTask: onCreateTask, renderTask: renderTask, overlappingTasksCount: overlappingTasksCount, children: tasksForSlot?.map((task) => (_jsx(CalendarItemWrapper, { task: task, position: task.position, onDragStart: (e) => handleDragStart(e, task), onDragEnd: handleDragEnd, onResize: onTaskResize, renderTask: renderTask }, task.id))) }, slotIndex));
|
|
55
|
+
return (_jsx(CalendarSlot, { slotIndex: slotIndex, onDrop: handleDrop, onCreateTask: onCreateTask, renderTask: renderTask, overlappingTasksCount: overlappingTasksCount, draggingTask: draggingTask || undefined, children: tasksForSlot?.map((task) => (_jsx(CalendarItemWrapper, { task: task, position: task.position, onDragStart: (e) => handleDragStart(e, task), onDragEnd: handleDragEnd, onResize: onTaskResize, renderTask: renderTask }, task.id))) }, slotIndex));
|
|
120
56
|
})] }));
|
|
121
57
|
};
|
|
@@ -73,9 +73,20 @@ export const Interactive = {
|
|
|
73
73
|
return t;
|
|
74
74
|
}));
|
|
75
75
|
};
|
|
76
|
-
const handleTaskResize = (taskId, newEstimatedTime) => {
|
|
77
|
-
console.log(`Task ${taskId} resized to ${newEstimatedTime} minutes`);
|
|
78
|
-
setTasks((prev) => prev.map((t) =>
|
|
76
|
+
const handleTaskResize = (taskId, newEstimatedTime, newStartHour, newStartMinutes) => {
|
|
77
|
+
console.log(`Task ${taskId} resized to ${newEstimatedTime} minutes. New start: ${newStartHour}:${newStartMinutes}`);
|
|
78
|
+
setTasks((prev) => prev.map((t) => {
|
|
79
|
+
if (t.id === taskId) {
|
|
80
|
+
const newTask = { ...t, estimatedTime: newEstimatedTime };
|
|
81
|
+
if (newStartHour !== undefined && newStartMinutes !== undefined) {
|
|
82
|
+
const newDate = new Date(t.dueDate);
|
|
83
|
+
newDate.setHours(newStartHour, newStartMinutes, 0, 0);
|
|
84
|
+
newTask.dueDate = newDate;
|
|
85
|
+
}
|
|
86
|
+
return newTask;
|
|
87
|
+
}
|
|
88
|
+
return t;
|
|
89
|
+
}));
|
|
79
90
|
};
|
|
80
91
|
const handleCreateTask = (hour, minutes, estimatedTime) => {
|
|
81
92
|
console.log(`Create task at ${hour}:${minutes} with duration ${estimatedTime}`);
|
|
@@ -92,7 +103,7 @@ export const Interactive = {
|
|
|
92
103
|
console.log(`Task ${taskId} clicked`);
|
|
93
104
|
alert(`Клик по задаче ${taskId}`);
|
|
94
105
|
};
|
|
95
|
-
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."] }), _jsxs("div", { style: { marginBottom: '20px', color: 'var(--secondary)', display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "checkbox", checked: showCompleted, onChange: (e) => setShowCompleted(e.target.checked), id: "show-completed" }), _jsx("label", { htmlFor: "show-completed", style: { cursor: 'pointer' }, children: "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044B\u0435" })] }), _jsx(CalendarLike, { tasks: filteredTasks, onTaskDrop: handleTaskDrop, onTaskResize: handleTaskResize, onCreateTask: handleCreateTask, renderTask: (task) => {
|
|
106
|
+
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. \u0418\u0437\u043C\u0435\u043D\u044F\u0439\u0442\u0435 \u0432\u0440\u0435\u043C\u044F \u043D\u0430\u0447\u0430\u043B\u0430 \u0438 \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C, \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u0437\u0430 \u0432\u0435\u0440\u0445\u043D\u0438\u0439 \u043A\u0440\u0430\u0439. ", _jsx("br", {}), "4. \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."] }), _jsxs("div", { style: { marginBottom: '20px', color: 'var(--secondary)', display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "checkbox", checked: showCompleted, onChange: (e) => setShowCompleted(e.target.checked), id: "show-completed" }), _jsx("label", { htmlFor: "show-completed", style: { cursor: 'pointer' }, children: "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044B\u0435" })] }), _jsx(CalendarLike, { tasks: filteredTasks, onTaskDrop: handleTaskDrop, onTaskResize: handleTaskResize, onCreateTask: handleCreateTask, renderTask: (task) => {
|
|
96
107
|
const isCompleted = task.isCompleted;
|
|
97
108
|
return (_jsx(CalendarItem, { title: _jsx(Cell, { subtitle: _jsx(Typo, { size: 'xs', weight: '500', children: "CODE-1" }), title: _jsx(Typo, { weight: "500", textDecoration: isCompleted ? 'line-through' : 'none', className: "truncate", children: task.title }), accLeft: [
|
|
98
109
|
{
|
|
@@ -3,7 +3,7 @@ import { animated, config, useSpring } from '@react-spring/web';
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { jc } from '../../../utils';
|
|
5
5
|
import { Typo } from '../text/typo';
|
|
6
|
-
export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, children, overlappingTasksCount = 0, }) => {
|
|
6
|
+
export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, children, overlappingTasksCount = 0, draggingTask, }) => {
|
|
7
7
|
const hour = Math.floor(slotIndex / 4);
|
|
8
8
|
const minutes = (slotIndex % 4) * 15;
|
|
9
9
|
const isTopSlot = slotIndex % 4 === 0;
|
|
@@ -22,8 +22,6 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
|
|
|
22
22
|
const handleDragOver = (e) => {
|
|
23
23
|
e.preventDefault();
|
|
24
24
|
e.dataTransfer.dropEffect = 'move';
|
|
25
|
-
// Пытаемся получить данные из глобальной переменной
|
|
26
|
-
const draggingTask = window._draggingTask;
|
|
27
25
|
if (!draggingTask)
|
|
28
26
|
return;
|
|
29
27
|
if (!dragOverInfo) {
|
|
@@ -37,7 +35,6 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
|
|
|
37
35
|
setDragOverInfo(null);
|
|
38
36
|
};
|
|
39
37
|
const handleInternalDrop = (e) => {
|
|
40
|
-
console.log('CalendarSlot: handleInternalDrop triggered', { hour, minutes });
|
|
41
38
|
setDragOverInfo(null);
|
|
42
39
|
onDrop?.(e, hour, minutes); // Приклеиваем к началу слота (15-минутка)
|
|
43
40
|
};
|
|
@@ -21,7 +21,7 @@ export type TCalendarLikeProps = {
|
|
|
21
21
|
autoScrollToCurrentTime?: boolean;
|
|
22
22
|
renderTask: (task: TCalendarTask) => ReactNode;
|
|
23
23
|
onTaskDrop?: (taskId: string, hour: number, minutes: number) => void;
|
|
24
|
-
onTaskResize?: (taskId: string, newEstimatedTime: number) => void;
|
|
24
|
+
onTaskResize?: (taskId: string, newEstimatedTime: number, newStartHour?: number, newStartMinutes?: number) => void;
|
|
25
25
|
onTaskClick?: (taskId: string) => void;
|
|
26
26
|
onCreateTask?: (hour: number, minutes: number, estimatedTime: number) => void;
|
|
27
27
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
export const useAutoScroll = (containerRef, enabled, topOffset) => {
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
if (enabled && containerRef.current) {
|
|
5
|
+
const scroll = (retryCount = 0) => {
|
|
6
|
+
if (!containerRef.current)
|
|
7
|
+
return;
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const hours = now.getHours();
|
|
10
|
+
const minutes = now.getMinutes();
|
|
11
|
+
const totalMinutes = hours * 60 + minutes;
|
|
12
|
+
// Находим ближайший скроллируемый родитель или используем окно
|
|
13
|
+
const scrollParent = (node) => {
|
|
14
|
+
if (!node)
|
|
15
|
+
return null;
|
|
16
|
+
const style = window.getComputedStyle(node);
|
|
17
|
+
const overflow = style.overflow + style.overflowY + style.overflowX;
|
|
18
|
+
if (/(auto|scroll)/.test(overflow))
|
|
19
|
+
return node;
|
|
20
|
+
return scrollParent(node.parentElement);
|
|
21
|
+
};
|
|
22
|
+
const parent = scrollParent(containerRef.current);
|
|
23
|
+
if (parent) {
|
|
24
|
+
const getRelativeOffsetTop = (target, ancestor) => {
|
|
25
|
+
let offset = 0;
|
|
26
|
+
let current = target;
|
|
27
|
+
while (current && current !== ancestor) {
|
|
28
|
+
offset += current.offsetTop;
|
|
29
|
+
current = current.offsetParent;
|
|
30
|
+
}
|
|
31
|
+
return offset;
|
|
32
|
+
};
|
|
33
|
+
const relativeTop = getRelativeOffsetTop(containerRef.current, parent);
|
|
34
|
+
// Если relativeTop равен 0 и это не начало дня, возможно DOM еще не готов
|
|
35
|
+
if (relativeTop === 0 && totalMinutes > 60 && retryCount < 5) {
|
|
36
|
+
setTimeout(() => scroll(retryCount + 1), 100);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
parent.scrollTo({
|
|
40
|
+
top: relativeTop + topOffset - parent.clientHeight / 2,
|
|
41
|
+
behavior: 'smooth',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const timeoutId = setTimeout(() => scroll(0), 100);
|
|
46
|
+
return () => clearTimeout(timeoutId);
|
|
47
|
+
}
|
|
48
|
+
}, [enabled, topOffset, containerRef]);
|
|
49
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
export const useCurrentTime = (updateInterval = 60000) => {
|
|
3
|
+
const [now, setNow] = useState(new Date());
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const interval = setInterval(() => {
|
|
6
|
+
setNow(new Date());
|
|
7
|
+
}, updateInterval);
|
|
8
|
+
return () => clearInterval(interval);
|
|
9
|
+
}, [updateInterval]);
|
|
10
|
+
const topOffset = useMemo(() => {
|
|
11
|
+
const hours = now.getHours();
|
|
12
|
+
const minutes = now.getMinutes();
|
|
13
|
+
const totalMinutes = hours * 60 + minutes;
|
|
14
|
+
// 15 минут = 20 пикселей (высота слота)
|
|
15
|
+
return totalMinutes * (20 / 15);
|
|
16
|
+
}, [now]);
|
|
17
|
+
return { now, topOffset };
|
|
18
|
+
};
|