Улучшения UI прогрессбаров и карточек
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m9s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m9s
This commit is contained in:
14
play-life-web/package-lock.json
generated
14
play-life-web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "3.8.9",
|
"version": "3.19.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "3.8.9",
|
"version": "3.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-circular-progressbar": "^2.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-easy-crop": "^5.5.6"
|
"react-easy-crop": "^5.5.6"
|
||||||
},
|
},
|
||||||
@@ -5913,6 +5914,15 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-circular-progressbar": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=0.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "3.19.0",
|
"version": "3.20.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-circular-progressbar": "^2.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-easy-crop": "^5.5.6"
|
"react-easy-crop": "^5.5.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,133 +1,108 @@
|
|||||||
import ProjectProgressBar from './ProjectProgressBar'
|
import ProjectProgressBar from './ProjectProgressBar'
|
||||||
import LoadingError from './LoadingError'
|
import LoadingError from './LoadingError'
|
||||||
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
||||||
|
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'
|
||||||
|
import 'react-circular-progressbar/dist/styles.css'
|
||||||
|
|
||||||
// Компонент круглого прогрессбара
|
// Компонент круглого прогрессбара с использованием react-circular-progressbar
|
||||||
function CircularProgressBar({ progress, size = 120, strokeWidth = 8, showCheckmark = true }) {
|
function CircularProgressBar({ progress, size = 120, strokeWidth = 8, showCheckmark = true, extraProgress = null, maxProgress = 100, textSize = 'large', displayProgress = null, textPosition = 'default', projectColor = null }) {
|
||||||
|
// Нормализуем прогресс для визуализации (0-100%)
|
||||||
const normalizedProgress = Math.min(Math.max(progress || 0, 0), 100)
|
const normalizedProgress = Math.min(Math.max(progress || 0, 0), 100)
|
||||||
const radius = (size - strokeWidth) / 2
|
|
||||||
const circumference = radius * 2 * Math.PI
|
// Если есть extra progress, вычисляем визуальный прогресс для overlay
|
||||||
const strokeDasharray = `${(normalizedProgress / 100) * circumference} ${circumference}`
|
const extraVisual = extraProgress !== null && extraProgress > 0
|
||||||
|
? Math.min((extraProgress / maxProgress) * 100, 100)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// Определяем, достигнут ли 100% или выше
|
||||||
|
const isComplete = (displayProgress !== null ? displayProgress : progress) >= 100
|
||||||
|
|
||||||
|
// Определяем градиент ID: зелёный если >= 100%, иначе по наличию extra progress
|
||||||
|
const gradientId = isComplete ? 'success-gradient' : (extraVisual > 0 ? 'project-gradient' : 'overall-gradient')
|
||||||
|
const extraGradientId = 'project-extra-gradient'
|
||||||
|
|
||||||
|
// Определяем класс размера текста
|
||||||
|
const textSizeClass = textSize === 'large' ? 'text-4xl' : textSize === 'small' ? 'text-base' : 'text-lg'
|
||||||
|
|
||||||
|
// Используем displayProgress если передан (может быть больше 100%), иначе progress
|
||||||
|
const progressToDisplay = displayProgress !== null ? displayProgress : progress
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
|
<div className="relative" style={{ width: size, height: size }}>
|
||||||
<svg className="transform -rotate-90" viewBox={`0 0 ${size} ${size}`}>
|
<CircularProgressbar
|
||||||
{/* Фоновая окружность */}
|
value={normalizedProgress}
|
||||||
<circle
|
strokeWidth={strokeWidth / size * 100}
|
||||||
cx={size / 2}
|
styles={buildStyles({
|
||||||
cy={size / 2}
|
// Цвета
|
||||||
r={radius}
|
pathColor: `url(#${gradientId})`,
|
||||||
stroke="currentColor"
|
trailColor: '#e5e7eb',
|
||||||
strokeWidth={strokeWidth}
|
// Анимация
|
||||||
fill="none"
|
pathTransitionDuration: 1,
|
||||||
className="text-gray-200"
|
// Размер текста (убираем встроенный)
|
||||||
|
textSize: '0px',
|
||||||
|
// Поворот, чтобы пустая часть была снизу
|
||||||
|
rotation: 0.625,
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
})}
|
||||||
|
// Создаем неполный круг (270 градусов)
|
||||||
|
circleRatio={0.75}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Extra progress overlay (если есть) */}
|
||||||
|
{extraVisual > 0 && (
|
||||||
|
<CircularProgressbar
|
||||||
|
value={extraVisual}
|
||||||
|
strokeWidth={strokeWidth / size * 100}
|
||||||
|
styles={buildStyles({
|
||||||
|
rotation: 0.625,
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
textSize: '0px',
|
||||||
|
pathColor: `url(#${extraGradientId})`,
|
||||||
|
trailColor: 'transparent',
|
||||||
|
pathTransitionDuration: 1,
|
||||||
|
})}
|
||||||
|
circleRatio={0.75}
|
||||||
|
className="absolute inset-0"
|
||||||
/>
|
/>
|
||||||
{/* Прогресс окружность */}
|
)}
|
||||||
<circle
|
|
||||||
cx={size / 2}
|
{/* Иконка статистики в центре */}
|
||||||
cy={size / 2}
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
r={radius}
|
<svg
|
||||||
stroke="url(#overall-gradient)"
|
width={size * 0.3}
|
||||||
strokeWidth={strokeWidth}
|
height={size * 0.3}
|
||||||
fill="none"
|
viewBox="0 0 24 24"
|
||||||
strokeDasharray={strokeDasharray}
|
fill="none"
|
||||||
strokeLinecap="round"
|
stroke="currentColor"
|
||||||
className="transition-all duration-1000 ease-out"
|
strokeWidth="2"
|
||||||
/>
|
strokeLinecap="round"
|
||||||
{/* Галочка при 100% */}
|
strokeLinejoin="round"
|
||||||
{showCheckmark && progress >= 100 && (
|
style={{
|
||||||
<g className="transform rotate-90" style={{ transformOrigin: `${size / 2}px ${size / 2}px` }}>
|
color: isComplete ? '#10b981' : '#4f46e5'
|
||||||
<path
|
}}
|
||||||
d={`M ${size / 2 - 12} ${size / 2} L ${size / 2 - 4} ${size / 2 + 8} L ${size / 2 + 12} ${size / 2 - 8}`}
|
>
|
||||||
stroke="url(#overall-gradient)"
|
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||||
strokeWidth="3"
|
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||||
strokeLinecap="round"
|
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||||||
strokeLinejoin="round"
|
</svg>
|
||||||
fill="none"
|
</div>
|
||||||
/>
|
|
||||||
</g>
|
{/* Кастомный текст снизу */}
|
||||||
)}
|
<div className={`absolute inset-0 flex justify-center items-end ${textPosition === 'lower' ? '' : 'pb-2'}`} style={textPosition === 'lower' ? { bottom: '0.125rem' } : {}}>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className={`${textSizeClass} font-bold`} style={{ color: isComplete ? '#10b981' : '#4f46e5' }}>
|
||||||
|
{progressToDisplay !== null && progressToDisplay !== undefined ? `${progressToDisplay.toFixed(0)}%` : 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Градиенты для SVG */}
|
||||||
|
<svg width="0" height="0">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="overall-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
<linearGradient id="overall-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
<stop offset="0%" stopColor="#4f46e5" />
|
<stop offset="0%" stopColor="#4f46e5" />
|
||||||
<stop offset="100%" stopColor="#9333ea" />
|
<stop offset="100%" stopColor="#9333ea" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
{/* Процент в центре */}
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-5xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
|
||||||
{progress !== null && progress !== undefined ? `${progress.toFixed(0)}%` : 'N/A'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Специальный компонент круглого прогрессбара с поддержкой экстра прогресса
|
|
||||||
function ProjectCircularProgressBar({ progress, extraProgress, size = 120, strokeWidth = 8, showCheckmark = true, percentage }) {
|
|
||||||
const normalizedProgress = Math.min(Math.max(progress || 0, 0), 100)
|
|
||||||
const normalizedExtraProgress = Math.min(Math.max(extraProgress || 0, 0), 100)
|
|
||||||
const radius = (size - strokeWidth) / 2
|
|
||||||
const circumference = radius * 2 * Math.PI
|
|
||||||
const strokeDasharray = `${(normalizedProgress / 100) * circumference} ${circumference}`
|
|
||||||
const extraStrokeDasharray = normalizedExtraProgress > 0 ? `${(normalizedExtraProgress / 100) * circumference} ${circumference}` : ''
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
|
|
||||||
<svg className="transform -rotate-90" viewBox={`0 0 ${size} ${size}`}>
|
|
||||||
{/* Фоновая окружность */}
|
|
||||||
<circle
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fill="none"
|
|
||||||
className="text-gray-200"
|
|
||||||
/>
|
|
||||||
{/* Базовый прогресс окружность */}
|
|
||||||
<circle
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke="url(#project-gradient)"
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeLinecap="round"
|
|
||||||
className="transition-all duration-1000 ease-out"
|
|
||||||
/>
|
|
||||||
{/* Экстра прогресс окружность (другой цвет) */}
|
|
||||||
{normalizedExtraProgress > 0 && (
|
|
||||||
<circle
|
|
||||||
cx={size / 2}
|
|
||||||
cy={size / 2}
|
|
||||||
r={radius}
|
|
||||||
stroke="url(#project-extra-gradient)"
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={extraStrokeDasharray}
|
|
||||||
strokeLinecap="round"
|
|
||||||
className="transition-all duration-1000 ease-out"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* Галочка при 100% */}
|
|
||||||
{showCheckmark && progress >= 100 && (
|
|
||||||
<g className="transform rotate-90" style={{ transformOrigin: `${size / 2}px ${size / 2}px` }}>
|
|
||||||
<path
|
|
||||||
d={`M ${size / 2 - 12} ${size / 2} L ${size / 2 - 4} ${size / 2 + 8} L ${size / 2 + 12} ${size / 2 - 8}`}
|
|
||||||
stroke="url(#project-gradient)"
|
|
||||||
strokeWidth="3"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
)}
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="project-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
<linearGradient id="project-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
<stop offset="0%" stopColor="#4f46e5" />
|
<stop offset="0%" stopColor="#4f46e5" />
|
||||||
<stop offset="100%" stopColor="#9333ea" />
|
<stop offset="100%" stopColor="#9333ea" />
|
||||||
@@ -136,18 +111,12 @@ function ProjectCircularProgressBar({ progress, extraProgress, size = 120, strok
|
|||||||
<stop offset="0%" stopColor="#f59e0b" />
|
<stop offset="0%" stopColor="#f59e0b" />
|
||||||
<stop offset="100%" stopColor="#d97706" />
|
<stop offset="100%" stopColor="#d97706" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<linearGradient id="success-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="#10b981" />
|
||||||
|
<stop offset="100%" stopColor="#059669" />
|
||||||
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
{/* Процент в центре */}
|
|
||||||
{percentage && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
|
||||||
{percentage}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -212,10 +181,14 @@ function ProjectCard({ project, projectColor, onProjectClick }) {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
// Для визуального отображения: 100% прогрессбара = максимум для данного приоритета
|
// Для визуального отображения: 100% прогрессбара = максимум для данного приоритета
|
||||||
const visualProgress = (goalProgress / maxProgressForPriority) * 100
|
// visualProgress показывает процент заполнения прогрессбара (0-100%), где 100% = maxProgressForPriority
|
||||||
const baseProgress = Math.min(goalProgress, 100) // Базовая часть (0-100%)
|
const visualProgress = Math.min((goalProgress / maxProgressForPriority) * 100, 100)
|
||||||
const extraProgress = Math.max(0, goalProgress - 100) // Экстра часть (свыше 100%)
|
|
||||||
const extraVisualProgress = (extraProgress / maxProgressForPriority) * 100 // Экстра часть в процентах от полного диапазона
|
// Для extra overlay: если goalProgress > 100%, показываем extra часть
|
||||||
|
// Но визуально это уже учтено в visualProgress, так что extra overlay не нужен
|
||||||
|
// Однако если нужно показать, что достигнут максимум, можно использовать другой подход
|
||||||
|
const baseVisualProgress = visualProgress
|
||||||
|
const extraVisualProgress = 0 // Не используем extra overlay, так как visualProgress уже показывает весь прогресс
|
||||||
|
|
||||||
// Вычисляем целевую зону
|
// Вычисляем целевую зону
|
||||||
const getTargetZone = () => {
|
const getTargetZone = () => {
|
||||||
@@ -239,32 +212,33 @@ function ProjectCard({ project, projectColor, onProjectClick }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className="bg-white rounded-2xl py-3 px-3 shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer border border-gray-200 hover:border-indigo-300"
|
className="bg-white rounded-3xl py-3 px-4 shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer border border-gray-200 hover:border-indigo-300"
|
||||||
>
|
>
|
||||||
{/* Верхняя часть с названием и прогрессом */}
|
{/* Верхняя часть с названием и прогрессом */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Левая часть - текст (название, баллы, целевая зона) */}
|
{/* Левая часть - текст (название, баллы, целевая зона) */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-base font-semibold text-gray-600 leading-normal truncate mb-1">
|
<div className="text-base font-semibold text-gray-600 leading-normal truncate mb-0.5">
|
||||||
{project_name}
|
{project_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-bold text-black leading-normal mb-1">
|
<div className="text-3xl font-bold text-black leading-normal mb-0.5">
|
||||||
{total_score?.toFixed(1) || '0.0'}
|
{total_score?.toFixed(1) || '0.0'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 leading-normal">
|
<div className="text-xs text-gray-500 leading-normal">
|
||||||
Целевая зона: {getTargetZone()}
|
Целевая зона: {getTargetZone()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Правая часть - круглый прогрессбар */}
|
{/* Правая часть - круглый прогрессбар */}
|
||||||
<div className="flex-shrink-0 ml-3">
|
<div className="flex-shrink-0 ml-3">
|
||||||
<ProjectCircularProgressBar
|
<CircularProgressBar
|
||||||
progress={visualProgress}
|
progress={baseVisualProgress}
|
||||||
extraProgress={extraVisualProgress}
|
|
||||||
size={80}
|
size={80}
|
||||||
strokeWidth={8}
|
strokeWidth={8}
|
||||||
showCheckmark={false}
|
textSize="small"
|
||||||
percentage={goalProgress.toFixed(0)}
|
displayProgress={goalProgress}
|
||||||
|
textPosition="lower"
|
||||||
|
projectColor={projectColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,6 +420,7 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
|
|||||||
progress={displayOverallProgress}
|
progress={displayOverallProgress}
|
||||||
size={180}
|
size={180}
|
||||||
strokeWidth={12}
|
strokeWidth={12}
|
||||||
|
showCheckmark={true}
|
||||||
/>
|
/>
|
||||||
{/* Подсказка при наведении */}
|
{/* Подсказка при наведении */}
|
||||||
<div className="absolute inset-0 rounded-full opacity-0 hover:opacity-100 transition-opacity duration-200 bg-black bg-opacity-10 flex items-center justify-center">
|
<div className="absolute inset-0 rounded-full opacity-0 hover:opacity-100 transition-opacity duration-200 bg-black bg-opacity-10 flex items-center justify-center">
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ function FullStatistics({ selectedProject, onClearSelection, data, loading, erro
|
|||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div style={{ height: '550px', paddingTop: '60px' }}>
|
<div style={{ height: '550px', paddingTop: '100px' }}>
|
||||||
<Line data={chartData} options={chartOptions} />
|
<Line data={chartData} options={chartOptions} />
|
||||||
</div>
|
</div>
|
||||||
<WeekProgressChart data={data} allProjectsSorted={getAllProjectsSorted(data)} currentWeekData={currentWeekData} selectedProject={selectedProject} />
|
<WeekProgressChart data={data} allProjectsSorted={getAllProjectsSorted(data)} currentWeekData={currentWeekData} selectedProject={selectedProject} />
|
||||||
|
|||||||
Reference in New Issue
Block a user