4.18.0: Улучшен календарь выбора даты
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m33s

This commit is contained in:
poignatov
2026-02-04 21:13:29 +03:00
parent f5e10c143f
commit 09ab87b6dd
5 changed files with 217 additions and 42 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "play-life-web",
"version": "3.28.1",
"version": "4.17.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "play-life-web",
"version": "3.28.1",
"version": "4.17.1",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -15,6 +15,7 @@
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-circular-progressbar": "^2.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "^18.2.0",
"react-easy-crop": "^5.5.6"
},
@@ -1618,6 +1619,12 @@
"node": ">=6.9.0"
}
},
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@@ -3871,6 +3878,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-jalali": {
"version": "4.1.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -5923,6 +5946,27 @@
"react": ">=0.14.0"
}
},
"node_modules/react-day-picker": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz",
"integrity": "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==",
"license": "MIT",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "4.17.1",
"version": "4.18.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -15,6 +15,7 @@
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-circular-progressbar": "^2.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "^18.2.0",
"react-easy-crop": "^5.5.6"
},

View File

@@ -277,7 +277,7 @@
}
.task-postpone-modal-overlay {
position: fixed;
position: fixed !important;
top: 0;
left: 0;
right: 0;
@@ -286,7 +286,8 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
z-index: 9999 !important;
padding: 1rem;
}
.task-postpone-modal {
@@ -336,6 +337,130 @@
padding: 0 1.5rem 1.5rem 1.5rem;
}
.task-postpone-calendar {
margin-bottom: 1rem;
}
/* Стили для react-day-picker v9 */
.task-postpone-calendar .rdp {
--rdp-cell-size: 40px;
--rdp-accent-color: #9ca3af;
--rdp-accent-background-color: #9ca3af;
--rdp-background-color: transparent;
--rdp-outline: none;
--rdp-outline-selected: none;
--rdp-selected-border: none;
--rdp-selected-font: inherit;
margin: 0;
}
/* Ячейка дня */
.task-postpone-calendar .rdp-day {
border-radius: 0.375rem;
transition: all 0.2s;
}
/* Кнопка внутри дня */
.task-postpone-calendar .rdp-day_button {
border-radius: 0.375rem;
transition: all 0.2s;
}
.task-postpone-calendar .rdp-day_button:hover:not([disabled]) {
background-color: #f3f4f6;
}
/* Сегодняшняя дата - жирный текст */
.task-postpone-calendar .rdp-today .rdp-day_button,
.task-postpone-calendar .rdp-day.rdp-today button,
.task-postpone-calendar [data-today="true"] button,
.task-postpone-calendar .rdp [data-today] button {
font-weight: 700 !important;
color: #1f2937;
}
/* Выбранная дата - серый маленький залитый круг */
.task-postpone-calendar .rdp-selected {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.task-postpone-calendar .rdp-selected .rdp-day_button {
border: none !important;
border-radius: 50% !important;
background-color: #e5e7eb !important;
color: #6b7280 !important;
font-weight: 400 !important;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
width: 28px !important;
height: 28px !important;
min-width: 28px !important;
min-height: 28px !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.task-postpone-calendar .rdp-selected .rdp-day_button:hover {
background-color: #d1d5db !important;
}
/* Если дата одновременно сегодняшняя и выбранная */
.task-postpone-calendar .rdp-today.rdp-selected .rdp-day_button {
color: #6b7280 !important;
background-color: #e5e7eb !important;
font-weight: 700 !important;
}
/* Недоступные даты (прошлые) */
.task-postpone-calendar .rdp-disabled .rdp-day_button {
opacity: 1 !important;
cursor: not-allowed;
color: #9ca3af !important;
background-color: transparent !important;
}
.task-postpone-calendar .rdp-disabled .rdp-day_button:hover {
background-color: transparent !important;
}
/* Дни из других месяцев */
.task-postpone-calendar .rdp-outside .rdp-day_button {
opacity: 0.3;
color: #9ca3af;
}
/* Заголовок календаря */
.task-postpone-calendar .rdp-caption_label {
font-size: 1rem;
font-weight: 600;
color: #1f2937;
}
.task-postpone-calendar .rdp-nav_button {
border-radius: 0.375rem;
transition: all 0.2s;
}
.task-postpone-calendar .rdp-nav_button:hover {
background-color: #f3f4f6;
}
/* Адаптивность для мобильных устройств */
@media (max-width: 640px) {
.task-postpone-calendar .rdp {
--rdp-cell-size: 36px;
}
.task-postpone-calendar .rdp-caption_label {
font-size: 0.875rem;
}
}
.task-postpone-quick-buttons {
display: flex;
gap: 0.5rem;

View File

@@ -1,8 +1,12 @@
import React, { useState, useEffect, useMemo, useRef } from 'react'
import { createPortal } from 'react-dom'
import { useAuth } from './auth/AuthContext'
import TaskDetail from './TaskDetail'
import LoadingError from './LoadingError'
import Toast from './Toast'
import { DayPicker } from 'react-day-picker'
import { ru } from 'react-day-picker/locale'
import 'react-day-picker/style.css'
import './TaskList.css'
const API_URL = '/api/tasks'
@@ -19,7 +23,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
const [isPostponing, setIsPostponing] = useState(false)
const [toast, setToast] = useState(null)
const [searchQuery, setSearchQuery] = useState('')
const dateInputRef = useRef(null)
useEffect(() => {
if (data) {
@@ -368,6 +371,26 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
setPostponeDate('')
}
const handleDateSelect = (date) => {
if (!date) return
// Преобразуем дату в формат YYYY-MM-DD
const formattedDate = formatDateToLocal(date)
setPostponeDate(formattedDate)
// Применяем дату и закрываем модальное окно
if (selectedTaskForPostpone) {
handlePostponeSubmitWithDate(formattedDate)
}
}
const handleDayClick = (day, modifiers) => {
// Обрабатываем клик даже если дата уже выбрана
if (day && !modifiers.disabled) {
handleDateSelect(day)
}
}
const handleTodayClick = () => {
const today = new Date()
today.setHours(0, 0, 0, 0)
@@ -985,7 +1008,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
const isToday = nextShowAtStr === todayStr
const isTomorrow = nextShowAtStr === tomorrowStr
return (
const modalContent = (
<div className="task-postpone-modal-overlay" onClick={handlePostponeClose}>
<div className="task-postpone-modal" onClick={(e) => e.stopPropagation()}>
<div className="task-postpone-modal-header">
@@ -995,41 +1018,19 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
</button>
</div>
<div className="task-postpone-modal-content">
<div className="task-postpone-input-group">
<input
ref={dateInputRef}
type="date"
value={postponeDate}
onChange={(e) => setPostponeDate(e.target.value)}
className="task-postpone-input"
min={new Date().toISOString().split('T')[0]}
<div className="task-postpone-calendar">
<DayPicker
mode="single"
selected={postponeDate ? new Date(postponeDate + 'T00:00:00') : undefined}
onSelect={handleDateSelect}
onDayClick={handleDayClick}
disabled={{ before: (() => {
const today = new Date()
today.setHours(0, 0, 0, 0)
return today
})() }}
locale={ru}
/>
<div
className="task-postpone-display-date"
onClick={() => {
// Открываем календарь при клике
if (dateInputRef.current) {
if (typeof dateInputRef.current.showPicker === 'function') {
dateInputRef.current.showPicker()
} else {
// Fallback для браузеров без showPicker
dateInputRef.current.focus()
dateInputRef.current.click()
}
}
}}
>
{postponeDate ? formatDateForDisplay(postponeDate) : 'Выберите дату'}
</div>
{postponeDate && (
<button
onClick={handlePostponeSubmit}
disabled={isPostponing || !postponeDate}
className="task-postpone-submit-checkmark"
>
</button>
)}
</div>
<div className="task-postpone-quick-buttons">
{!isToday && (
@@ -1055,6 +1056,10 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
</div>
</div>
)
return typeof document !== 'undefined'
? createPortal(modalContent, document.body)
: modalContent
})()}
</div>
)