4.16.0: Добавлен выбор цвета для проектов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m9s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m9s
This commit is contained in:
63
play-life-web/src/components/ColorPickerModal.jsx
Normal file
63
play-life-web/src/components/ColorPickerModal.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { PROJECT_COLORS_PALETTE } from '../utils/projectUtils'
|
||||
import './Integrations.css'
|
||||
|
||||
function ColorPickerModal({ onClose, onColorSelect, currentColor }) {
|
||||
const handleColorClick = (color) => {
|
||||
onColorSelect(color)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={onClose}>
|
||||
<div className="bg-white rounded-lg max-w-md w-90 shadow-lg max-h-[90vh] flex flex-col" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Заголовок с кнопкой закрытия */}
|
||||
<div className="flex justify-between items-center p-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-800">Выберите цвет проекта</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-white hover:bg-gray-100 text-gray-600 hover:text-gray-800 border border-gray-200 hover:border-gray-300 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||
title="Закрыть"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Контент - сетка цветов */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
{PROJECT_COLORS_PALETTE.map((color, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleColorClick(color)}
|
||||
className={`
|
||||
w-12 h-12 rounded-full
|
||||
border-2 transition-all duration-200
|
||||
hover:scale-110 hover:shadow-lg
|
||||
${currentColor === color
|
||||
? 'border-gray-800 shadow-md ring-2 ring-offset-2 ring-gray-400'
|
||||
: 'border-gray-300 hover:border-gray-500'
|
||||
}
|
||||
`}
|
||||
style={{ backgroundColor: color }}
|
||||
title={color}
|
||||
>
|
||||
{currentColor === color && (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColorPickerModal
|
||||
@@ -234,7 +234,7 @@ function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick
|
||||
{projects.map((project, index) => {
|
||||
if (!project || !project.project_name) return null
|
||||
|
||||
const projectColor = getProjectColor(project.project_name, allProjects)
|
||||
const projectColor = getProjectColor(project.project_name, allProjects, project.color)
|
||||
|
||||
return (
|
||||
<ProjectCard
|
||||
|
||||
@@ -22,11 +22,13 @@ import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import LoadingError from './LoadingError'
|
||||
import Toast from './Toast'
|
||||
import ColorPickerModal from './ColorPickerModal'
|
||||
import './Integrations.css'
|
||||
|
||||
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
||||
const PROJECTS_API_URL = '/projects'
|
||||
const PRIORITY_UPDATE_API_URL = '/project/priority'
|
||||
const PROJECT_COLOR_API_URL = '/project/color'
|
||||
const PROJECT_MOVE_API_URL = '/project/move'
|
||||
const PROJECT_CREATE_API_URL = '/project/create'
|
||||
|
||||
@@ -257,7 +259,7 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess, onError }
|
||||
}
|
||||
|
||||
// Компонент для сортируемого элемента проекта
|
||||
function SortableProjectItem({ project, index, allProjects, onMenuClick }) {
|
||||
function SortableProjectItem({ project, index, allProjects, onMenuClick, onColorClick }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@@ -274,7 +276,7 @@ function SortableProjectItem({ project, index, allProjects, onMenuClick }) {
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
const projectColor = getProjectColor(project.name, allProjects)
|
||||
const projectColor = getProjectColor(project.name, allProjects, project.color)
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -301,10 +303,17 @@ function SortableProjectItem({ project, index, allProjects, onMenuClick }) {
|
||||
<circle cx="13" cy="13" r="1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (onColorClick) {
|
||||
onColorClick(project, e)
|
||||
}
|
||||
}}
|
||||
className="w-3 h-3 rounded-full flex-shrink-0 hover:scale-125 transition-transform cursor-pointer border border-gray-300 hover:border-gray-500"
|
||||
style={{ backgroundColor: projectColor }}
|
||||
></div>
|
||||
title="Выбрать цвет"
|
||||
></button>
|
||||
<span className="font-semibold text-gray-800">{project.name}</span>
|
||||
</div>
|
||||
{onMenuClick && (
|
||||
@@ -347,7 +356,7 @@ function DroppableSlot({ containerId, isEmpty, maxItems, currentCount }) {
|
||||
}
|
||||
|
||||
// Компонент для слота приоритета
|
||||
function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = null, containerId, onAddClick }) {
|
||||
function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = null, containerId, onAddClick, onColorClick }) {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="text-sm font-semibold text-gray-600 mb-2">{title}</div>
|
||||
@@ -362,6 +371,7 @@ function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = nu
|
||||
index={index}
|
||||
allProjects={allProjects}
|
||||
onMenuClick={onMenuClick}
|
||||
onColorClick={onColorClick}
|
||||
/>
|
||||
))}
|
||||
{onAddClick && containerId === 'low' && (
|
||||
@@ -405,6 +415,8 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
const [selectedProject, setSelectedProject] = useState(null) // Для модального окна
|
||||
const [showMoveScreen, setShowMoveScreen] = useState(false) // Для экрана переноса
|
||||
const [showAddScreen, setShowAddScreen] = useState(false) // Для экрана добавления
|
||||
const [showColorPicker, setShowColorPicker] = useState(false) // Для модального окна выбора цвета
|
||||
const [selectedProjectForColor, setSelectedProjectForColor] = useState(null) // Проект для выбора цвета
|
||||
|
||||
|
||||
const scrollContainerRef = useRef(null)
|
||||
@@ -438,7 +450,8 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
|
||||
const id = item?.project_id ?? item?.id ?? item?.projectId ?? null
|
||||
const priorityValue = item?.priority ?? item?.priority_value ?? item?.priority_level ?? null
|
||||
return { id, name, priority: priorityValue }
|
||||
const color = item?.color ?? null
|
||||
return { id, name, priority: priorityValue, color }
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -458,7 +471,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
const low = []
|
||||
|
||||
uniqueProjects.forEach(item => {
|
||||
const projectEntry = { name: item.name, id: item.id }
|
||||
const projectEntry = { name: item.name, id: item.id, color: item.color }
|
||||
if (item.priority === 1) {
|
||||
max.push(projectEntry)
|
||||
} else if (item.priority === 2) {
|
||||
@@ -860,6 +873,38 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
}
|
||||
}
|
||||
|
||||
const handleColorClick = (project, e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedProjectForColor(project)
|
||||
setShowColorPicker(true)
|
||||
}
|
||||
|
||||
const handleColorSelect = async (color) => {
|
||||
if (!selectedProjectForColor) return
|
||||
|
||||
try {
|
||||
const projectId = selectedProjectForColor.id ?? selectedProjectForColor.name
|
||||
const response = await authFetch(PROJECT_COLOR_API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: projectId, color: color }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(errorText || 'Ошибка при сохранении цвета')
|
||||
}
|
||||
|
||||
setShowColorPicker(false)
|
||||
setSelectedProjectForColor(null)
|
||||
// Обновляем список проектов
|
||||
fetchProjects()
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения цвета:', error)
|
||||
setToastMessage({ text: error.message || 'Ошибка сохранения цвета', type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setSelectedProject(null)
|
||||
}
|
||||
@@ -912,6 +957,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
projects={maxPriority}
|
||||
allProjects={allProjects}
|
||||
onMenuClick={handleMenuClick}
|
||||
onColorClick={handleColorClick}
|
||||
maxItems={1}
|
||||
containerId="max"
|
||||
/>
|
||||
@@ -923,6 +969,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
projects={mediumPriority}
|
||||
allProjects={allProjects}
|
||||
onMenuClick={handleMenuClick}
|
||||
onColorClick={handleColorClick}
|
||||
maxItems={2}
|
||||
containerId="medium"
|
||||
/>
|
||||
@@ -934,6 +981,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
projects={lowPriority}
|
||||
allProjects={allProjects}
|
||||
onMenuClick={handleMenuClick}
|
||||
onColorClick={handleColorClick}
|
||||
containerId="low"
|
||||
onAddClick={() => setShowAddScreen(true)}
|
||||
/>
|
||||
@@ -954,7 +1002,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: getProjectColor(activeProject.name, allProjects) }}
|
||||
style={{ backgroundColor: getProjectColor(activeProject.name, allProjects, activeProject.color) }}
|
||||
></div>
|
||||
<span className="font-semibold text-gray-800">{activeProject.name}</span>
|
||||
</div>
|
||||
@@ -1030,6 +1078,18 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно выбора цвета */}
|
||||
{showColorPicker && selectedProjectForColor && (
|
||||
<ColorPickerModal
|
||||
onClose={() => {
|
||||
setShowColorPicker(false)
|
||||
setSelectedProjectForColor(null)
|
||||
}}
|
||||
onColorSelect={handleColorSelect}
|
||||
currentColor={selectedProjectForColor.color || getProjectColor(selectedProjectForColor.name, allProjects)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{toastMessage && (
|
||||
<Toast
|
||||
message={toastMessage.text}
|
||||
|
||||
@@ -152,12 +152,17 @@ function WeekProgressChart({ data, allProjectsSorted, currentWeekData, selectedP
|
||||
|
||||
// Используем абсолютные значения (баллы)
|
||||
// Сортируем проекты так же, как в полной статистике (по priority и min_goal_score)
|
||||
// Получаем цвет проекта из данных full-statistics, если доступен
|
||||
const projectsWithData = weekProjects.map(project => {
|
||||
const color = getProjectColor(project.projectName, allProjects)
|
||||
// Ищем цвет проекта в данных full-statistics
|
||||
const projectData = data?.find(item => item.project_name === project.projectName)
|
||||
const projectColor = projectData?.color
|
||||
? getProjectColor(project.projectName, allProjects, projectData.color)
|
||||
: getProjectColor(project.projectName, allProjects)
|
||||
|
||||
return {
|
||||
...project,
|
||||
color
|
||||
color: projectColor
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
// Утилиты для работы с проектами - обеспечивают единую сортировку и цвета
|
||||
|
||||
// Палитра из 30 контрастных цветов для проектов (HEX формат)
|
||||
// Должна быть синхронизирована с backend (main.go)
|
||||
export const PROJECT_COLORS_PALETTE = [
|
||||
'#EF4444', // Красный
|
||||
'#F97316', // Оранжевый
|
||||
'#F59E0B', // Янтарный
|
||||
'#EAB308', // Желтый
|
||||
'#84CC16', // Лайм
|
||||
'#22C55E', // Зеленый
|
||||
'#10B981', // Изумрудный
|
||||
'#14B8A6', // Бирюзовый
|
||||
'#06B6D4', // Голубой
|
||||
'#0EA5E9', // Небесный
|
||||
'#3B82F6', // Синий
|
||||
'#6366F1', // Индиго
|
||||
'#8B5CF6', // Фиолетовый
|
||||
'#A855F7', // Пурпурный
|
||||
'#D946EF', // Фуксия
|
||||
'#EC4899', // Розовый
|
||||
'#F43F5E', // Розово-красный
|
||||
'#DC2626', // Темно-красный
|
||||
'#EA580C', // Темно-оранжевый
|
||||
'#CA8A04', // Темно-желтый
|
||||
'#65A30D', // Темно-лайм
|
||||
'#16A34A', // Темно-зеленый
|
||||
'#059669', // Темно-изумрудный
|
||||
'#0D9488', // Темно-бирюзовый
|
||||
'#0891B2', // Темно-голубой
|
||||
'#0284C7', // Темно-небесный
|
||||
'#2563EB', // Темно-синий
|
||||
'#4F46E5', // Темно-индиго
|
||||
'#7C3AED', // Темно-фиолетовый
|
||||
'#9333EA', // Темно-пурпурный
|
||||
]
|
||||
|
||||
// Функция для генерации цвета проекта на основе его индекса в отсортированном списке
|
||||
export function getProjectColorByIndex(index) {
|
||||
const hue = (index * 137.508) % 360 // Золотой угол для равномерного распределения цветов
|
||||
@@ -39,13 +74,20 @@ export function getAllProjectsSorted(allProjectsData, currentWeekData = null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает цвет проекта на основе его названия
|
||||
* Получает цвет проекта на основе его названия или цвета из БД
|
||||
*
|
||||
* @param {string} projectName - название проекта
|
||||
* @param {Array} allProjectsSorted - отсортированный список всех проектов
|
||||
* @returns {string} цвет в формате HSL
|
||||
* @param {string|null} projectColorFromDB - цвет проекта из базы данных (HEX формат)
|
||||
* @returns {string} цвет в формате HEX или HSL (fallback)
|
||||
*/
|
||||
export function getProjectColor(projectName, allProjectsSorted) {
|
||||
export function getProjectColor(projectName, allProjectsSorted, projectColorFromDB = null) {
|
||||
// Если передан цвет из БД и он не пустой - использовать его
|
||||
if (projectColorFromDB && projectColorFromDB.trim() !== '') {
|
||||
return projectColorFromDB
|
||||
}
|
||||
|
||||
// Иначе использовать вычисляемый цвет (текущая логика) - это fallback для обратной совместимости
|
||||
const projectIndex = allProjectsSorted.indexOf(projectName)
|
||||
return projectIndex >= 0 ? getProjectColorByIndex(projectIndex) : '#9CA3AF'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user