change marking page layout
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "no-copy-frontend",
|
"name": "no-copy-frontend",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 2999",
|
"dev": "next dev -p 2999",
|
||||||
|
|||||||
@@ -2,18 +2,23 @@ import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
|
|||||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||||
import TestSection from '@/app/ui/marking-page/test-section';
|
import TestSection from '@/app/ui/marking-page/test-section';
|
||||||
import UploadSectionImage from '@/app/ui/marking-page/upload-section-image';
|
import UploadSectionImage from '@/app/ui/marking-page/upload-section-image';
|
||||||
import { IconImageFile } from '@/app/ui/icons/icons';
|
import TanstakFilesTable from '@/app/components/tanstakTable';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const t = useTranslations('Global');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-title">
|
<div className="page-title">
|
||||||
<h1 >Защита изображений</h1>
|
<h1>
|
||||||
|
{t('image-protection')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<ProtectionSummary />
|
<ProtectionSummary />
|
||||||
<UploadSectionImage />
|
<UploadSectionImage />
|
||||||
<TestSection />
|
<TestSection />
|
||||||
<ProtectedFilesTable />
|
<TanstakFilesTable/>
|
||||||
|
{/* <ProtectedFilesTable /> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { BarChart, Bar, Cell, XAxis, Tooltip } from 'recharts';
|
||||||
|
|
||||||
|
type Files = {
|
||||||
|
data: {
|
||||||
|
name: string,
|
||||||
|
checks: number,
|
||||||
|
violations: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarShapeProps {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
fill: string;
|
||||||
|
radius?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StackedBarChart = ({ data }: Files) => {
|
||||||
|
const t = useTranslations('Global');
|
||||||
|
|
||||||
|
const editedData = data.map(e => {
|
||||||
|
return {
|
||||||
|
name: e.name,
|
||||||
|
checks: e.checks - e.violations,
|
||||||
|
violations: e.violations
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mainColors = ['#0088FE20', '#00C49F20', '#FFBB2820', '#FF804220', '#FF000020', '#FFC0CB20'];
|
||||||
|
const secondColors = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', 'red', 'pink'];
|
||||||
|
|
||||||
|
const strokeColor = "#fff";
|
||||||
|
const strokeWidth = 1;
|
||||||
|
|
||||||
|
const renderBarWithPartialBorder = (props: BarShapeProps) => {
|
||||||
|
const { x, y, width, height, fill } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Основной прямоугольник без обводки */}
|
||||||
|
<rect
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Левая граница */}
|
||||||
|
<line
|
||||||
|
x1={x}
|
||||||
|
y1={y}
|
||||||
|
x2={x}
|
||||||
|
y2={y + height}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Правая граница */}
|
||||||
|
<line
|
||||||
|
x1={x + width}
|
||||||
|
y1={y}
|
||||||
|
x2={x + width}
|
||||||
|
y2={y + height}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Нижняя граница */}
|
||||||
|
<line
|
||||||
|
x1={x}
|
||||||
|
y1={y + height}
|
||||||
|
x2={x + width}
|
||||||
|
y2={y + height}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBarWithTopBorder = (props: BarShapeProps) => {
|
||||||
|
const { x, y, width, height, fill } = props;
|
||||||
|
const borderRadius = 10;
|
||||||
|
|
||||||
|
if (height <= 0 || width <= 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Основной прямоугольник с закруглением ТОЛЬКО ВЕРХНИХ углов */}
|
||||||
|
<path
|
||||||
|
d={`
|
||||||
|
M ${x + borderRadius} ${y}
|
||||||
|
L ${x + width - borderRadius} ${y}
|
||||||
|
Q ${x + width} ${y} ${x + width} ${y + borderRadius}
|
||||||
|
L ${x + width} ${y + height}
|
||||||
|
L ${x} ${y + height}
|
||||||
|
L ${x} ${y + borderRadius}
|
||||||
|
Q ${x} ${y} ${x + borderRadius} ${y}
|
||||||
|
Z
|
||||||
|
`}
|
||||||
|
fill={fill}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Верхняя граница */}
|
||||||
|
<path
|
||||||
|
d={`
|
||||||
|
M ${x + borderRadius} ${y}
|
||||||
|
L ${x + width - borderRadius} ${y}
|
||||||
|
`}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Левая граница */}
|
||||||
|
<line
|
||||||
|
x1={x}
|
||||||
|
y1={y + borderRadius}
|
||||||
|
x2={x}
|
||||||
|
y2={y + height}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Правая граница */}
|
||||||
|
<line
|
||||||
|
x1={x + width}
|
||||||
|
y1={y + borderRadius}
|
||||||
|
x2={x + width}
|
||||||
|
y2={y + height}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Левый верхний закругленный угол */}
|
||||||
|
<path
|
||||||
|
d={`
|
||||||
|
M ${x} ${y + borderRadius}
|
||||||
|
Q ${x} ${y} ${x + borderRadius} ${y}
|
||||||
|
`}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Правый верхний закругленный угол */}
|
||||||
|
<path
|
||||||
|
d={`
|
||||||
|
M ${x + width - borderRadius} ${y}
|
||||||
|
Q ${x + width} ${y} ${x + width} ${y + borderRadius}
|
||||||
|
`}
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BarChart
|
||||||
|
style={{ width: '100%', maxWidth: '700px', maxHeight: '210px', aspectRatio: 1.618 }}
|
||||||
|
responsive
|
||||||
|
data={editedData}
|
||||||
|
margin={{
|
||||||
|
top: 20,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <Tooltip /> */}
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
stroke="#fff"
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="checks"
|
||||||
|
stackId="a"
|
||||||
|
label={{ position: 'top', fill: '#fff' }}
|
||||||
|
shape={renderBarWithPartialBorder as any}
|
||||||
|
>
|
||||||
|
{data.map((_entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={secondColors[index % 20]} />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
<Bar
|
||||||
|
dataKey="violations"
|
||||||
|
stackId="a"
|
||||||
|
label={{ position: 'top', fill: '#fff' }}
|
||||||
|
shape={renderBarWithTopBorder as any}
|
||||||
|
>
|
||||||
|
{data.map((_entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={mainColors[index % 20]} />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -61,19 +61,28 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
color: #6366f1;
|
color: #6366f1;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 5px;
|
||||||
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 5px;
|
||||||
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(45deg, #6566f1, #01579b);
|
background: linear-gradient(45deg, #6566f1, #5a5ce9);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,17 @@ export function IconAudioFile() {
|
|||||||
|
|
||||||
export function IconShield() {
|
export function IconShield() {
|
||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22q-3.475-.875-5.738-3.988T4 11.1V5l8-3l8 3v6.1q0 3.8-2.262 6.913T12 22m0-2.1q2.6-.825 4.3-3.3t1.7-5.5V6.375l-6-2.25l-6 2.25V11.1q0 3.025 1.7 5.5t4.3 3.3M10 16h4q.425 0 .713-.288T15 15v-3q0-.425-.288-.712T14 11v-1q0-.825-.587-1.412T12 8t-1.412.588T10 10v1q-.425 0-.712.288T9 12v3q0 .425.288.713T10 16m1-5v-1q0-.425.288-.712T12 9t.713.288T13 10v1z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
|
||||||
|
<path fill="currentColor" d="M12 22q-3.475-.875-5.738-3.988T4 11.1V5l8-3l8 3v6.1q0 3.8-2.262 6.913T12 22m0-2.1q2.6-.825 4.3-3.3t1.7-5.5V6.375l-6-2.25l-6 2.25V11.1q0 3.025 1.7 5.5t4.3 3.3M10 16h4q.425 0 .713-.288T15 15v-3q0-.425-.288-.712T14 11v-1q0-.825-.587-1.412T12 8t-1.412.588T10 10v1q-.425 0-.712.288T9 12v3q0 .425.288.713T10 16m1-5v-1q0-.425.288-.712T12 9t.713.288T13 10v1z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconShieldAdd() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16" className="icon">
|
||||||
|
<path fill="currentColor" d="M7.647 2.146a.5.5 0 0 1 .708 0C9.595 3.39 10.969 4 12.5 4a.5.5 0 0 1 .5.5v1.1a5.5 5.5 0 0 0-1-.393v-.226c-1.48-.112-2.815-.726-4-1.792c-1.186 1.066-2.52 1.68-4 1.792v2.52c0 1.431.361 2.56 1.017 3.44c.053.66.222 1.288.487 1.862C3.844 11.59 3 9.81 3 7.502V4.5a.5.5 0 0 1 .5-.5c1.53 0 2.904-.611 4.147-1.854M15 10.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4-2a.5.5 0 0 0-1 0V10H8.5a.5.5 0 0 0 0 1H10v1.5a.5.5 0 0 0 1 0V11h1.5a.5.5 0 0 0 0-1H11z" />
|
||||||
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* removed */
|
||||||
|
|
||||||
import { IconShield, IconDownload } from '@/app/ui/icons/icons';
|
import { IconShield, IconDownload } from '@/app/ui/icons/icons';
|
||||||
export default function ProtectedFilesTable() {
|
export default function ProtectedFilesTable() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,21 +1,92 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
name: 'Page A',
|
||||||
|
checks: 4000,
|
||||||
|
violations: 2000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page B',
|
||||||
|
checks: 3000,
|
||||||
|
violations: 1398
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page C',
|
||||||
|
checks: 2000,
|
||||||
|
violations: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page D',
|
||||||
|
checks: 2780,
|
||||||
|
violations: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page E',
|
||||||
|
checks: 5090,
|
||||||
|
violations: 4800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page F',
|
||||||
|
checks: 5390,
|
||||||
|
violations: 3800
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export default function ProtectionSummary() {
|
export default function ProtectionSummary() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const t = useTranslations('Global');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="protection-summary">
|
<div className="protection-overview">
|
||||||
<h3>Статистика защиты</h3>
|
<h3>{t('protecting-your-content')}</h3>
|
||||||
<div className="protection-stats">
|
<p>{t('current-status-of')}</p>
|
||||||
<div className="stat-card">
|
<div className={`protection-stats ${isOpen ? "opened" : ""}`}>
|
||||||
<div className="stat-number">0</div>
|
<div className="protection-stat total-files">
|
||||||
<div className="stat-label">Всего файлов</div>
|
<div className="protection-stat-value">0</div>
|
||||||
|
<div className="protection-stat-label">{t('total-files')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-number">0</div>
|
<div className="protection-stat total-checks">
|
||||||
<div className="stat-label">Общий размер</div>
|
<div className="protection-stat-value">0</div>
|
||||||
</div>
|
<div className="protection-stat-label">{t('size')}</div>
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-number">0</div>
|
|
||||||
<div className="stat-label">Проверки/нарушения</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{isOpen ? (
|
||||||
|
<div className="protection-stat total-usage">
|
||||||
|
<StackedBarChart data={data} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="protection-stat total-usage">
|
||||||
|
<div className="protection-stat-value">0</div>
|
||||||
|
<div className="protection-stat-label">
|
||||||
|
{t('size')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="protection-overview-switch cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useRef, useState, useCallback, ChangeEvent, DragEvent } from 'react';
|
import React, { useRef, useState, useCallback, ChangeEvent, DragEvent } from 'react';
|
||||||
|
import {IconShieldAdd} from '@/app/ui/icons/icons';
|
||||||
|
|
||||||
interface SelectedFile {
|
interface SelectedFile {
|
||||||
file: File;
|
file: File;
|
||||||
@@ -188,11 +189,9 @@ export default function UploadSectionImage() {
|
|||||||
transition: 'all .2s ease-in-out'
|
transition: 'all .2s ease-in-out'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<IconShieldAdd />
|
||||||
<h4>Перетащите изображения сюда</h4>
|
<h4>Перетащите изображения сюда</h4>
|
||||||
<p className="description">
|
<p className="mb-2.5 text-[#6b7280]">или</p>
|
||||||
Размер: 150x150 - 4000x4000 пикселей, форматы: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -210,6 +209,16 @@ export default function UploadSectionImage() {
|
|||||||
Выбрать файлы для защиты
|
Выбрать файлы для защиты
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<p className="description">
|
||||||
|
Разрешение изображение: 100x100 - 10000x10000 пикселей
|
||||||
|
</p>
|
||||||
|
<p className="description">
|
||||||
|
Размер файла: до 20 МБ
|
||||||
|
</p>
|
||||||
|
<p className="description">
|
||||||
|
Формат файла: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается)
|
||||||
|
</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
<p className="text-red-600 font-medium">❌ {error}</p>
|
<p className="text-red-600 font-medium">❌ {error}</p>
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
"shown": "Shown",
|
"shown": "Shown",
|
||||||
"date-filter": "Date filter",
|
"date-filter": "Date filter",
|
||||||
"type-filter": "Type filter",
|
"type-filter": "Type filter",
|
||||||
"items-per-page": "Items per page"
|
"items-per-page": "Items per page",
|
||||||
|
"image-protection": "Защита изображений"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
"shown": "Показано",
|
"shown": "Показано",
|
||||||
"date-filter": "Фильтр по дате",
|
"date-filter": "Фильтр по дате",
|
||||||
"type-filter": "Фильтр по типу",
|
"type-filter": "Фильтр по типу",
|
||||||
"items-per-page": "Записей на странице"
|
"items-per-page": "Записей на странице",
|
||||||
|
"image-protection": "Защита изображений"
|
||||||
},
|
},
|
||||||
"Login-register-form": {
|
"Login-register-form": {
|
||||||
"and": "и",
|
"and": "и",
|
||||||
|
|||||||
Reference in New Issue
Block a user