add image upload, fix styles
This commit is contained in:
Generated
+16
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
|
"@vkid/sdk": "^2.6.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jose": "^6.1.2",
|
"jose": "^6.1.2",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
@@ -2542,6 +2543,15 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@vkid/sdk": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vkid/sdk/-/sdk-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-TCaGh6BvQNY8Atru0KJXdGuwm70y9WS2xBOLNripHjYMEcft0BMIiLKUvJ7pAeJOtk/15MvPaUIEmBS2Vt0NBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-js": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -3080,6 +3090,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
|
"@vkid/sdk": "^2.6.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"jose": "^6.1.2",
|
"jose": "^6.1.2",
|
||||||
"next": "^16.0.7",
|
"next": "^16.0.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use server'
|
'use server'
|
||||||
import { getSessionData } from '@/app/lib/session';
|
import { getSessionData } from '@/app/actions/session';
|
||||||
import { localDevelopmentUrl } from '@/app/lib/definitions';
|
import { localDevelopmentUrl } from '@/app/actions/definitions';
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NODE_ENV === 'development'
|
const API_BASE_URL = process.env.NODE_ENV === 'development'
|
||||||
? localDevelopmentUrl
|
? localDevelopmentUrl
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
|
||||||
import { SignupFormSchema, loginFormSchema, localDevelopmentUrl } from '@/app/lib/definitions';
|
import { SignupFormSchema, loginFormSchema, localDevelopmentUrl } from '@/app/actions/definitions';
|
||||||
import { createSession, deleteSession, getSessionData } from '@/app/lib/session';
|
import { createSession, deleteSession, getSessionData } from '@/app/actions/session';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NODE_ENV === 'development'
|
const API_BASE_URL = process.env.NODE_ENV === 'development'
|
||||||
|
|||||||
@@ -25,18 +25,22 @@ export const SignupFormSchema = z
|
|||||||
email: z.email({ error: 'Пожалуйста, введите правильный адрес электронной почты.' }).trim(),
|
email: z.email({ error: 'Пожалуйста, введите правильный адрес электронной почты.' }).trim(),
|
||||||
phone: z
|
phone: z
|
||||||
.string()
|
.string()
|
||||||
.min(12, { error: 'Пожалуйста, введите телефонный номер' }),
|
.min(12, { error: 'Пожалуйста, введите телефонный номер.' }),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, { error: 'Длина пароля должна быть не менее 8 символов.' })
|
.min(8, { error: 'Длина пароля должна быть не менее 8 символов.' })
|
||||||
.regex(/[a-zA-Z]/, { error: 'Пароль должен содержать как минимум 1 букву.' })
|
.regex(/[a-zA-Z]/, { error: 'Пароль должен содержать как минимум 1 букву.' })
|
||||||
.regex(/[0-9]/, { error: 'Пароль должен содержать как минимум 1 цифру.' })
|
.regex(/[0-9]/, { error: 'Пароль должен содержать как минимум 1 цифру.' })
|
||||||
|
.refine((value) => !/[а-яёА-ЯЁ]/.test(value),
|
||||||
|
{
|
||||||
|
message: 'Пароль не должен содержать кириллицу.'
|
||||||
|
})
|
||||||
.trim(),
|
.trim(),
|
||||||
confirm_password: z
|
confirm_password: z
|
||||||
.string(),
|
.string(),
|
||||||
agree: z
|
agree: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, { error: 'Вы должны ознакомиться с условиями использования и политикой конфиденциальности' })
|
.min(2, { error: 'Пожалуйста, ознакомьтесь с условиями использования и политикой конфиденциальности.' })
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.confirm_password, {
|
.refine((data) => data.password === data.confirm_password, {
|
||||||
message: 'Повторите пароль',
|
message: 'Повторите пароль',
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'server-only'
|
import 'server-only'
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import { decrypt } from '@/app/lib/session'
|
import { decrypt } from '@/app/actions/session'
|
||||||
|
|
||||||
export async function updateSession() {
|
export async function updateSession() {
|
||||||
const session = (await cookies()).get('session')?.value
|
const session = (await cookies()).get('session')?.value
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import NavLinks from '@/app/ui/nav-links'
|
import NavLinks from '@/app/ui/nav-links'
|
||||||
import styles from '@/app/page.module.scss'
|
import styles from '@/app/styles/page.module.scss'
|
||||||
import HeaderPanel from '../ui/header/headerPanel';
|
import HeaderPanel from '../ui/header/headerPanel';
|
||||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||||
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||||
import { getUserData } from '@/app/lib/action';
|
import { getUserData } from '@/app/actions/action';
|
||||||
|
|
||||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const queryClient = getQueryClient();
|
const queryClient = getQueryClient();
|
||||||
@@ -19,7 +19,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
<NavLinks />
|
<NavLinks />
|
||||||
<div className={`${styles.mainContainter}`}>
|
<div className={`${styles['main-containter']}`}>
|
||||||
<HeaderPanel />
|
<HeaderPanel />
|
||||||
<main>
|
<main>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
|
||||||
|
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||||
|
import TestSection from '@/app/ui/marking-page/test-section';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="page-title">Защита аудио</h1>
|
||||||
|
<ProtectionSummary />
|
||||||
|
<TestSection />
|
||||||
|
<ProtectedFilesTable />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
|
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 UploadSection from '@/app/ui/marking-page/upload-section';
|
import UploadSectionImage from '@/app/ui/marking-page/upload-section-image';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">🛡️ Защита изображений</h1>
|
<h1 className="page-title">Защита изображений</h1>
|
||||||
<ProtectionSummary />
|
<ProtectionSummary />
|
||||||
<UploadSection />
|
<UploadSectionImage />
|
||||||
<TestSection />
|
<TestSection />
|
||||||
<ProtectedFilesTable />
|
<ProtectedFilesTable />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
|
||||||
|
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||||
|
import TestSection from '@/app/ui/marking-page/test-section';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="page-title">Защита видео</h1>
|
||||||
|
<ProtectionSummary />
|
||||||
|
<TestSection />
|
||||||
|
<ProtectedFilesTable />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -65,6 +65,10 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
|
|
||||||
|
&[type="password"]::-ms-reveal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-options {
|
.form-options {
|
||||||
@@ -74,6 +78,12 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
color: #fb2c36;
|
||||||
|
font-size: .875rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-checkbox {
|
.form-checkbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -66,13 +66,65 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.description {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(45deg, #6566f1, #01579b);
|
background: linear-gradient(45deg, #6566f1, #01579b);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-file {
|
||||||
|
border-radius: 15px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&-file-info {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-image {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
max-height: calc(4px * 40);
|
||||||
|
margin-inline: auto;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background: linear-gradient(45deg, #6566f1, #01579b);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: 0.3s;
|
||||||
|
min-width: 160px;
|
||||||
|
transform: translateY(0px);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #fff;
|
||||||
|
color: rgb(99, 102, 241);
|
||||||
|
border: 2px solid rgb(99, 102, 241);
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: 0.3s;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px #6366f14d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.mainContainter {
|
.main-containter {
|
||||||
margin-left: 280px;
|
margin-left: 280px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
color: white;
|
color: white;
|
||||||
transform: translateX(5px)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
@@ -40,6 +39,10 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-dropdown {
|
.nav-dropdown {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { fetchFruits } from '@/app/lib/action';
|
import { fetchFruits } from '@/app/actions/action';
|
||||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getUserData } from '@/app/lib/action';
|
import { getUserData } from '@/app/actions/action';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { logout } from '@/app/actions/auth';
|
import { logout } from '@/app/actions/auth';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Config, OneTap } from '@vkid/sdk';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
const VKAuthDynamic = () => {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const isInitialized = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Динамическая загрузка SDK
|
||||||
|
const loadVKIDSDK = async () => {
|
||||||
|
try {
|
||||||
|
// Проверяем, не загружен ли уже SDK
|
||||||
|
if (window.VKIDSDK) return window.VKIDSDK;
|
||||||
|
|
||||||
|
const { default: VKID } = await import('@vkid/sdk');
|
||||||
|
window.VKIDSDK = VKID;
|
||||||
|
return VKID;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load VKID SDK:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
if (isInitialized.current) return;
|
||||||
|
|
||||||
|
const VKID = await loadVKIDSDK();
|
||||||
|
if (!VKID || !containerRef.current) return;
|
||||||
|
|
||||||
|
isInitialized.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Конфигурация
|
||||||
|
VKID.Config.init({
|
||||||
|
app: Number(process.env.NEXT_PUBLIC_VK_APP_ID) || 1111111,
|
||||||
|
redirectUrl: process.env.NEXT_PUBLIC_REDIRECT_URL || window.location.origin,
|
||||||
|
responseMode: VKID.ConfigResponseMode.Callback,
|
||||||
|
source: VKID.ConfigSource.LOWCODE,
|
||||||
|
scope: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создание и рендеринг виджета
|
||||||
|
const oneTap = new VKID.OneTap();
|
||||||
|
|
||||||
|
oneTap
|
||||||
|
.render({
|
||||||
|
container: containerRef.current,
|
||||||
|
showAlternativeLogin: true,
|
||||||
|
})
|
||||||
|
.on(VKID.WidgetEvents.ERROR, handleError)
|
||||||
|
.on(VKID.OneTapInternalEvents.LOGIN_SUCCESS, async (payload) => {
|
||||||
|
const { code, device_id } = payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authData = await VKID.Auth.exchangeCode(code, device_id);
|
||||||
|
handleSuccess(authData);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('VKID initialization error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Очистка
|
||||||
|
isInitialized.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSuccess = async (data) => {
|
||||||
|
try {
|
||||||
|
// Отправка данных на ваш API endpoint
|
||||||
|
const response = await fetch('/api/auth/vk-callback', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Успешная авторизация
|
||||||
|
console.log('Authentication successful:', result);
|
||||||
|
// Перенаправление или обновление состояния
|
||||||
|
window.location.href = '/dashboard'; // Пример редиректа
|
||||||
|
} else {
|
||||||
|
handleError(result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Server request failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (error) => {
|
||||||
|
console.error('Authentication error:', error);
|
||||||
|
// Можно показать уведомление пользователю
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div ref={containerRef} />
|
||||||
|
vk
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VKAuthDynamic;
|
||||||
@@ -23,7 +23,7 @@ export default function LoginForm() {
|
|||||||
defaultValue={state?.previousState?.email ? state?.previousState?.email : ''}
|
defaultValue={state?.previousState?.email ? state?.previousState?.email : ''}
|
||||||
/>
|
/>
|
||||||
{state?.error?.email && (
|
{state?.error?.email && (
|
||||||
<p className="text-sm text-red-500">{state?.error?.email}</p>
|
<p className={`${styles['form-error']}`}>{state?.error?.email}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`${styles['form-group']}`}>
|
<div className={`${styles['form-group']}`}>
|
||||||
@@ -36,11 +36,11 @@ export default function LoginForm() {
|
|||||||
placeholder="Введите пароль"
|
placeholder="Введите пароль"
|
||||||
/>
|
/>
|
||||||
{state?.error?.password && (
|
{state?.error?.password && (
|
||||||
<p className="text-sm text-red-500">{state?.error?.password}</p>
|
<p className={`${styles['form-error']}`}>{state?.error?.password}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{state?.error.server && (
|
{state?.error.server && (
|
||||||
<p className="text-sm text-red-500">{state?.error.server}</p>
|
<p className={`${styles['form-error']}`}>{state?.error.server}</p>
|
||||||
)}
|
)}
|
||||||
<div className={`${styles['form-options']}`}>
|
<div className={`${styles['form-options']}`}>
|
||||||
<div className={`${styles['form-checkbox']}`}>
|
<div className={`${styles['form-checkbox']}`}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import style from "./ui.module.scss"
|
import style from "@/app/styles/ui.module.scss"
|
||||||
|
|
||||||
export default function LogoIcon() {
|
export default function LogoIcon() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import styles from './ui.module.scss';
|
import styles from '@/app/styles/ui.module.scss';
|
||||||
|
|
||||||
export default function Logo() {
|
export default function Logo() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,15 +9,11 @@ export default function ProtectionSummary() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-number">0</div>
|
<div className="stat-number">0</div>
|
||||||
<div className="stat-label">Защищено</div>
|
<div className="stat-label">Общий размер</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-number">0</div>
|
<div className="stat-number">0</div>
|
||||||
<div className="stat-label">Изображений</div>
|
<div className="stat-label">Проверки/нарушения</div>
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-number">100%</div>
|
|
||||||
<div className="stat-label">Уровень защиты</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useRef, useState, useCallback, ChangeEvent, DragEvent } from 'react';
|
||||||
|
|
||||||
|
// Типы для выбранного файла
|
||||||
|
interface SelectedFile {
|
||||||
|
file: File;
|
||||||
|
preview: string;
|
||||||
|
name: string;
|
||||||
|
size: string;
|
||||||
|
dimensions?: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupportedFormat = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/bmp';
|
||||||
|
type FileExtension = 'jpg' | 'jpeg' | 'png' | 'gif' | 'bmp';
|
||||||
|
|
||||||
|
export default function UploadSectionImage() {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const SUPPORTED_FORMATS: SupportedFormat[] = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/bmp'
|
||||||
|
];
|
||||||
|
|
||||||
|
const ALLOWED_EXTENSIONS: FileExtension[] = ['jpg', 'jpeg', 'png', 'gif', 'bmp'];
|
||||||
|
|
||||||
|
|
||||||
|
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
|
||||||
|
if (!SUPPORTED_FORMATS.includes(file.type as SupportedFormat)) {
|
||||||
|
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
if (!extension || !ALLOWED_EXTENSIONS.includes(extension as FileExtension)) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
errorMessage: 'Неподдерживаемый формат файла.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SIZE = 10 * 1024 * 1024;
|
||||||
|
if (file.size > MAX_SIZE) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
errorMessage: 'Файл слишком большой. Максимальный размер: 10MB'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const dimensions = {
|
||||||
|
width: img.width,
|
||||||
|
height: img.height
|
||||||
|
};
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
resolve(dimensions);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
reject(new Error('Не удалось загрузить изображение'));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = objectUrl;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
|
||||||
|
if (!file) {
|
||||||
|
setError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const validation = validateFile(file);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
setError(validation.errorMessage || 'Неизвестная ошибка валидации');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dimensions = await getImageDimensions(file);
|
||||||
|
|
||||||
|
if (dimensions.width < 150 || dimensions.height < 150 ||
|
||||||
|
dimensions.width > 4000 || dimensions.height > 4000) {
|
||||||
|
setError(`Размер изображения: ${dimensions.width}x${dimensions.height}. Допустимый диапазон: 150x150 - 4000x4000 пикселей.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание превью
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
setSelectedFile({
|
||||||
|
file,
|
||||||
|
preview: objectUrl,
|
||||||
|
name: file.name,
|
||||||
|
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||||
|
dimensions
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Файл обработан:', {
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
dimensions
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Ошибка при обработке файла');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const file = event.target.files?.[0] || null;
|
||||||
|
handleFileSelect(file);
|
||||||
|
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonClick = (): void => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (): void => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
handleFileSelect(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearFile = (): void => {
|
||||||
|
if (selectedFile) {
|
||||||
|
URL.revokeObjectURL(selectedFile.preview);
|
||||||
|
}
|
||||||
|
setSelectedFile(null);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (selectedFile) {
|
||||||
|
URL.revokeObjectURL(selectedFile.preview);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [selectedFile]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="upload-section">
|
||||||
|
<h3>📷 Загрузить изображение для защиты</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
style={{
|
||||||
|
border: isDragging ? '2px dashed #3b82f6' : '2px dashed #d1d5db',
|
||||||
|
backgroundColor: isDragging ? '#eff6ff' : '#f9fafb',
|
||||||
|
padding: '2rem',
|
||||||
|
borderRadius: '.75rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
transition: 'all .2s ease-in-out'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4>Перетащите изображения сюда</h4>
|
||||||
|
<p className="description">
|
||||||
|
Размер: 150x150 - 4000x4000 пикселей, форматы: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".jpg,.jpeg,.png,.gif,.bmp"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
aria-label="Выбор файла для защиты"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Выбрать файлы для защиты
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-600 font-medium">❌ {error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedFile && (
|
||||||
|
<div
|
||||||
|
className="selected-file"
|
||||||
|
>
|
||||||
|
<div className="selected-file-file-info">
|
||||||
|
<p className="text-gray-700">
|
||||||
|
<span className="font-medium">Файл:</span> {selectedFile.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
<span className="font-medium">Размер:</span> {selectedFile.size}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
<span className="font-medium">Стоимость защиты:</span> 0
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700">
|
||||||
|
<span className="font-medium">Текущий баланс:</span> 0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={selectedFile.preview}
|
||||||
|
alt="Предпросмотр загруженного изображения"
|
||||||
|
className="uploaded-image"
|
||||||
|
onError={() => setError('Не удалось загрузить превью изображения')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-3 justify-center">
|
||||||
|
<button
|
||||||
|
className="btn btn-cancel"
|
||||||
|
onClick={handleClearFile}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-confirm"
|
||||||
|
onClick={() => {
|
||||||
|
console.log('Обработка файла:', selectedFile.file);
|
||||||
|
console.log('Файл отправлен на защиту!');
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Подтвердить защиту
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
export default function UploadSection() {
|
|
||||||
return (
|
|
||||||
<div className="upload-section">
|
|
||||||
<h3>
|
|
||||||
📷 Загрузить изображение для защиты
|
|
||||||
</h3>
|
|
||||||
<div className="drag-drop-zone">
|
|
||||||
<div className="text-[48px] mb-[15px]">🛡️</div>
|
|
||||||
<h4>
|
|
||||||
Перетащите изображения сюда
|
|
||||||
</h4>
|
|
||||||
<p>
|
|
||||||
Размер: 150x150 - 4000x4000 пикселей, форматы: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается)
|
|
||||||
</p>
|
|
||||||
<button className="btn btn-primary">
|
|
||||||
🛡️ Выбрать файлы для защиты
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import styles from '@/app/ui/ui.module.scss';
|
import styles from '@/app/styles/ui.module.scss';
|
||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
@@ -19,27 +19,16 @@ export default function DropDownList() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Маркировка видео',
|
name: 'Маркировка видео',
|
||||||
href: '/pages/emptypage'
|
href: '/pages/marking-video'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Маркировка аудио',
|
name: 'Маркировка аудио',
|
||||||
href: '/pages/emptypage'
|
href: '/pages/marking-audio'
|
||||||
},
|
}
|
||||||
{
|
|
||||||
name: 'Маркировка PDF',
|
|
||||||
href: '/pages/emptypage'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Маркировка PHP/JS',
|
|
||||||
href: '/pages/emptypage'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Маркировка Текста',
|
|
||||||
href: '/pages/emptypage'
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
${styles['nav-dropdown']}
|
${styles['nav-dropdown']}
|
||||||
@@ -56,6 +45,7 @@ export default function DropDownList() {
|
|||||||
<path d="M7 10l5 5 5-5z"></path>
|
<path d="M7 10l5 5 5-5z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ul className={`
|
<ul className={`
|
||||||
${styles['sub-menu']}
|
${styles['sub-menu']}
|
||||||
${dropDownExpanded ? styles['expanded'] : ''}
|
${dropDownExpanded ? styles['expanded'] : ''}
|
||||||
@@ -77,6 +67,6 @@ export default function DropDownList() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import styles from './ui.module.scss'
|
import styles from '@/app/styles/ui.module.scss'
|
||||||
import Logo from '@/app/ui/logo';
|
import Logo from '@/app/ui/logo';
|
||||||
import DropDownList from '@/app/ui/nav-link-dropdown';
|
import DropDownList from '@/app/ui/nav-link-dropdown';
|
||||||
import { logout } from '@/app/actions/auth';
|
import { logout } from '@/app/actions/auth';
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { getUserData } from '@/app/lib/action';
|
import { getUserData } from '@/app/actions/action';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const {
|
const {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse, NextRequest } from 'next/server';
|
import { NextResponse, NextRequest } from 'next/server';
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import { decrypt } from '@/app/lib/session';
|
import { decrypt } from '@/app/actions/session';
|
||||||
|
|
||||||
const protectedRoutes = ['/pages']
|
const protectedRoutes = ['/pages']
|
||||||
const publicRoutes = ['/login', '/register', '/']
|
const publicRoutes = ['/login', '/register', '/']
|
||||||
|
|||||||
Reference in New Issue
Block a user