detail page
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,4 +1,4 @@
|
|||||||
import { PlagiarismDetailPage } from '@/widgets/detail/ui/detailPage';
|
import PlagiatResult from '@/widgets/detail/ui/PlagiatResult';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{ detail: string }>;
|
params: Promise<{ detail: string }>;
|
||||||
@@ -6,5 +6,6 @@ interface Props {
|
|||||||
|
|
||||||
export default async function DetailPage({ params }: Props) {
|
export default async function DetailPage({ params }: Props) {
|
||||||
const { detail } = await params;
|
const { detail } = await params;
|
||||||
return <PlagiarismDetailPage checkId={detail} />;
|
console.log(detail);
|
||||||
|
return <PlagiatResult />;
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/image/index.ts
Normal file
1
src/image/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Logo_image } from './logo.png';
|
||||||
BIN
src/image/logo.png
Normal file
BIN
src/image/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -1,6 +1,6 @@
|
|||||||
export const links = {
|
export const links = {
|
||||||
login: '/users/login/',
|
login: '/users/login/',
|
||||||
register: '/users/register/',
|
register: '/users/register/',
|
||||||
plagiarismCheck: '/plagiarism/check/',
|
plagiarismCheck: '/shared/documents/',
|
||||||
history: '/shared/documents/list/',
|
history: '/shared/documents/list/',
|
||||||
};
|
};
|
||||||
|
|||||||
460
src/widgets/detail/ui/PlagiatResult.tsx
Normal file
460
src/widgets/detail/ui/PlagiatResult.tsx
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface Source {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
matchPercentage: number;
|
||||||
|
matchedWords: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckResult {
|
||||||
|
ai: number;
|
||||||
|
plagiarism: number;
|
||||||
|
originality: number;
|
||||||
|
citation: number;
|
||||||
|
checkedWords: number;
|
||||||
|
uniqueWords: number;
|
||||||
|
lexicalUniqueness: number;
|
||||||
|
sentences: number;
|
||||||
|
avgWordsPerSentence: number;
|
||||||
|
lines: number;
|
||||||
|
sources: Source[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Certificate {
|
||||||
|
id: string;
|
||||||
|
issuedAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
verificationCode: string;
|
||||||
|
issuerName: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlagiatResultProps {
|
||||||
|
name: string;
|
||||||
|
initials: string;
|
||||||
|
email: string;
|
||||||
|
location: string;
|
||||||
|
fileName: string;
|
||||||
|
checkedAt: string;
|
||||||
|
result: CheckResult;
|
||||||
|
certificate: Certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blue palette
|
||||||
|
const blue = {
|
||||||
|
50: '#E6F1FB',
|
||||||
|
100: '#B5D4F4',
|
||||||
|
200: '#85B7EB',
|
||||||
|
400: '#378ADD',
|
||||||
|
600: '#185FA5',
|
||||||
|
800: '#0C447C',
|
||||||
|
900: '#042C53',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockData: PlagiatResultProps = {
|
||||||
|
name: 'Sokhibjon Orzikulov',
|
||||||
|
initials: 'SO',
|
||||||
|
email: 'sakhib@orzklv.uz',
|
||||||
|
location: 'Tashkent, Uzbekistan',
|
||||||
|
fileName: 'resume_sokhibjon.pdf',
|
||||||
|
checkedAt: '2026-04-02',
|
||||||
|
result: {
|
||||||
|
ai: 86,
|
||||||
|
plagiarism: 92,
|
||||||
|
originality: 5,
|
||||||
|
citation: 3,
|
||||||
|
checkedWords: 1477,
|
||||||
|
uniqueWords: 593,
|
||||||
|
lexicalUniqueness: 40,
|
||||||
|
sentences: 105,
|
||||||
|
avgWordsPerSentence: 14.1,
|
||||||
|
lines: 191,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
url: 'https://arxiv.org/abs/1706.03762',
|
||||||
|
title: 'arxiv.org — Attention Is All You Need',
|
||||||
|
matchPercentage: 9,
|
||||||
|
matchedWords: 937,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://en.wikipedia.org/wiki/Machine_learning',
|
||||||
|
title: 'Wikipedia — Machine Learning',
|
||||||
|
matchPercentage: 7,
|
||||||
|
matchedWords: 730,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://towardsdatascience.com/introduction-to-neural-networks',
|
||||||
|
title: 'Towards Data Science — Introduction to Neural Networks',
|
||||||
|
matchPercentage: 6,
|
||||||
|
matchedWords: 625,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
certificate: {
|
||||||
|
id: 'cert-9001',
|
||||||
|
issuedAt: '2026-03-30',
|
||||||
|
expiresAt: '2027-03-30',
|
||||||
|
verificationCode: 'PLAG-9001-VERIFY',
|
||||||
|
issuerName: 'Global Plagiarism Checker',
|
||||||
|
downloadUrl: '/certificates/cert-9001.pdf',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function CircleGauge({ value }: { value: number }) {
|
||||||
|
const r = 50;
|
||||||
|
const circ = 2 * Math.PI * r;
|
||||||
|
const offset = circ - (value / 100) * circ;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="130"
|
||||||
|
height="130"
|
||||||
|
viewBox="0 0 130 130"
|
||||||
|
aria-label={`${value}% plagiat`}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="65"
|
||||||
|
cy="65"
|
||||||
|
r={r}
|
||||||
|
fill="none"
|
||||||
|
stroke={blue[100]}
|
||||||
|
strokeWidth="14"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="65"
|
||||||
|
cy="65"
|
||||||
|
r={r}
|
||||||
|
fill="none"
|
||||||
|
stroke={blue[900]}
|
||||||
|
strokeWidth="14"
|
||||||
|
strokeDasharray={circ}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
transform="rotate(-90 65 65)"
|
||||||
|
style={{ transition: 'stroke-dashoffset 0.8s ease' }}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="65"
|
||||||
|
y="60"
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="26"
|
||||||
|
fontWeight="500"
|
||||||
|
fill={blue[900]}
|
||||||
|
>
|
||||||
|
{value}%
|
||||||
|
</text>
|
||||||
|
<text x="65" y="78" textAnchor="middle" fontSize="11" fill={blue[400]}>
|
||||||
|
plagiat
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BarRow({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2.5">
|
||||||
|
<div className="flex justify-between items-center text-sm mb-1">
|
||||||
|
<span className="flex items-center gap-1.5 text-slate-500">
|
||||||
|
<span
|
||||||
|
className="inline-block w-2.5 h-2.5 rounded-full"
|
||||||
|
style={{ background: color }}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium" style={{ color: blue[900] }}>
|
||||||
|
{value}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="h-1.5 rounded-full overflow-hidden"
|
||||||
|
style={{ background: blue[100] }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full"
|
||||||
|
style={{
|
||||||
|
width: `${value}%`,
|
||||||
|
background: color,
|
||||||
|
transition: 'width 0.8s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricCard({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg p-3.5" style={{ background: blue[50] }}>
|
||||||
|
<p className="text-xs mb-1" style={{ color: blue[600] }}>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-medium" style={{ color: blue[900] }}>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SourceItem({ source, index }: { source: Source; index: number }) {
|
||||||
|
const colors = [blue[900], blue[600], blue[400]];
|
||||||
|
const color = colors[index] ?? blue[400];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-lg p-3"
|
||||||
|
style={{ border: `0.5px solid ${blue[100]}` }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span
|
||||||
|
className="text-sm font-medium truncate max-w-[72%]"
|
||||||
|
style={{ color: blue[900] }}
|
||||||
|
>
|
||||||
|
{source.title}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium" style={{ color }}>
|
||||||
|
{source.matchPercentage}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] mb-1.5 truncate" style={{ color: blue[400] }}>
|
||||||
|
{source.url}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="h-1.5 rounded-full overflow-hidden"
|
||||||
|
style={{ background: blue[100] }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full"
|
||||||
|
style={{
|
||||||
|
width: `${source.matchPercentage}%`,
|
||||||
|
background: color,
|
||||||
|
transition: 'width 0.8s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function PlagiatResult({
|
||||||
|
data = mockData,
|
||||||
|
}: {
|
||||||
|
data?: PlagiatResultProps;
|
||||||
|
}) {
|
||||||
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
const { result, certificate } = data;
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
setDownloading(true);
|
||||||
|
setTimeout(() => setDownloading(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const divider = (
|
||||||
|
<hr
|
||||||
|
style={{
|
||||||
|
borderColor: blue[100],
|
||||||
|
borderTopWidth: '0.5px',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
margin: '1.25rem 0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-start justify-center p-6 bg-slate-50">
|
||||||
|
<div
|
||||||
|
className="w-full max-w-2xl rounded-xl p-5"
|
||||||
|
style={{ background: '#ffffff', border: `0.5px solid ${blue[100]}` }}
|
||||||
|
>
|
||||||
|
{/* ── Header ── */}
|
||||||
|
<div className="flex items-center gap-3 mb-5">
|
||||||
|
<div
|
||||||
|
className="w-11 h-11 rounded-full flex items-center justify-center text-sm font-medium shrink-0"
|
||||||
|
style={{ background: blue[50], color: blue[600] }}
|
||||||
|
>
|
||||||
|
{data.initials}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span
|
||||||
|
className="text-[15px] font-medium"
|
||||||
|
style={{ color: blue[900] }}
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-xs px-2.5 py-0.5 rounded-md font-medium"
|
||||||
|
style={{ background: blue[100], color: blue[800] }}
|
||||||
|
>
|
||||||
|
{result.plagiarism}% plagiat
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-[13px] mt-0.5 truncate"
|
||||||
|
style={{ color: blue[400] }}
|
||||||
|
>
|
||||||
|
{data.fileName} · {data.email} · {data.location}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
<p className="text-[11px]" style={{ color: blue[400] }}>
|
||||||
|
Tekshirilgan
|
||||||
|
</p>
|
||||||
|
<p className="text-[12px] mt-0.5" style={{ color: blue[600] }}>
|
||||||
|
{data.checkedAt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{divider}
|
||||||
|
|
||||||
|
{/* ── Top metrics ── */}
|
||||||
|
<div className="grid grid-cols-4 gap-2.5 mb-5">
|
||||||
|
<MetricCard
|
||||||
|
label="Plagiat darajasi"
|
||||||
|
value={`${result.plagiarism}%`}
|
||||||
|
/>
|
||||||
|
<MetricCard label="AI yozgan" value={`${result.ai}%`} />
|
||||||
|
<MetricCard label="Originallik" value={`${result.originality}%`} />
|
||||||
|
<MetricCard label="Iqtibos" value={`${result.citation}%`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Gauge + bars ── */}
|
||||||
|
<div className="flex items-center gap-5 mb-5">
|
||||||
|
<CircleGauge value={result.plagiarism} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<BarRow
|
||||||
|
label="Plagiat"
|
||||||
|
value={result.plagiarism}
|
||||||
|
color={blue[900]}
|
||||||
|
/>
|
||||||
|
<BarRow
|
||||||
|
label="AI generatsiya"
|
||||||
|
value={result.ai}
|
||||||
|
color={blue[600]}
|
||||||
|
/>
|
||||||
|
<BarRow
|
||||||
|
label="Original"
|
||||||
|
value={result.originality}
|
||||||
|
color={blue[400]}
|
||||||
|
/>
|
||||||
|
<BarRow label="Iqtibos" value={result.citation} color={blue[200]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{divider}
|
||||||
|
|
||||||
|
{/* ── Text analysis ── */}
|
||||||
|
<p
|
||||||
|
className="text-[13px] font-medium uppercase tracking-wider mb-3"
|
||||||
|
style={{ color: blue[400] }}
|
||||||
|
>
|
||||||
|
Matn tahlili
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-2 mb-5">
|
||||||
|
<MetricCard
|
||||||
|
label="Jami so'z"
|
||||||
|
value={result.checkedWords.toLocaleString()}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Unikal so'z"
|
||||||
|
value={result.uniqueWords.toLocaleString()}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Leksik unikalligi"
|
||||||
|
value={`${result.lexicalUniqueness}%`}
|
||||||
|
/>
|
||||||
|
<MetricCard label="Jumlalar" value={String(result.sentences)} />
|
||||||
|
<MetricCard
|
||||||
|
label="O'rt. so'z/juml."
|
||||||
|
value={String(result.avgWordsPerSentence)}
|
||||||
|
/>
|
||||||
|
<MetricCard label="Qatorlar" value={String(result.lines)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{divider}
|
||||||
|
|
||||||
|
{/* ── Sources ── */}
|
||||||
|
<p
|
||||||
|
className="text-[13px] font-medium uppercase tracking-wider mb-3"
|
||||||
|
style={{ color: blue[400] }}
|
||||||
|
>
|
||||||
|
Manba yo‘nalishlari
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-2.5 mb-5">
|
||||||
|
{result.sources.map((source, i) => (
|
||||||
|
<SourceItem key={source.url} source={source} index={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{divider}
|
||||||
|
|
||||||
|
{/* ── Certificate ── */}
|
||||||
|
<p
|
||||||
|
className="text-[13px] font-medium uppercase tracking-wider mb-3"
|
||||||
|
style={{ color: blue[400] }}
|
||||||
|
>
|
||||||
|
Sertifikat
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 rounded-lg p-3.5"
|
||||||
|
style={{ background: blue[50], border: `0.5px solid ${blue[100]}` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
|
||||||
|
style={{ background: blue[600] }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9 1L11.2 6.5H17L12.4 10L14.1 16L9 12.8L3.9 16L5.6 10L1 6.5H6.8L9 1Z"
|
||||||
|
fill="#E6F1FB"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-[13px] font-medium" style={{ color: blue[900] }}>
|
||||||
|
{certificate.issuerName} sertifikati
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="text-[11px] mt-0.5 font-mono truncate"
|
||||||
|
style={{ color: blue[600] }}
|
||||||
|
>
|
||||||
|
{certificate.verificationCode} · Amal qilish:{' '}
|
||||||
|
{certificate.expiresAt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="text-xs px-3 py-1.5 rounded-md shrink-0 transition-colors cursor-pointer"
|
||||||
|
style={{
|
||||||
|
border: `0.5px solid ${blue[400]}`,
|
||||||
|
color: blue[600],
|
||||||
|
background: downloading ? blue[100] : 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{downloading ? 'Yuklanmoqda...' : 'Yuklab olish ↗'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,9 +24,11 @@ export interface PlagiarismSubmissionResponse {
|
|||||||
// ─── Form State Types ────────────────────────────────────────────────────────
|
// ─── Form State Types ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface PlagiarismFormState {
|
export interface PlagiarismFormState {
|
||||||
topic: string;
|
title: string;
|
||||||
file: File | null;
|
file: File | null;
|
||||||
withCertificate: boolean;
|
certificate: boolean;
|
||||||
|
text?: string;
|
||||||
|
total_price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlagiarismFormErrors = Partial<
|
export type PlagiarismFormErrors = Partial<
|
||||||
@@ -39,6 +41,5 @@ export type SubmissionStatus = 'idle' | 'loading' | 'success' | 'error';
|
|||||||
|
|
||||||
export interface SubmissionState {
|
export interface SubmissionState {
|
||||||
status: SubmissionStatus;
|
status: SubmissionStatus;
|
||||||
response: PlagiarismSubmissionResponse | null;
|
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,30 +5,31 @@ import {
|
|||||||
PlagiarismFormState,
|
PlagiarismFormState,
|
||||||
SubmissionState,
|
SubmissionState,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { selectFullName, useUserStore } from './userStore';
|
|
||||||
import { isFormValid, validatePlagiarismForm } from './validation';
|
import { isFormValid, validatePlagiarismForm } from './validation';
|
||||||
import { submitPlagiarismCheck } from '@/shared/request/plagiarismapi';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { links } from '@/shared/request/links';
|
||||||
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
|
|
||||||
// ─── Initial States ──────────────────────────────────────────────────────────
|
// ─── Initial States ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const INITIAL_FORM: PlagiarismFormState = {
|
const INITIAL_FORM: PlagiarismFormState = {
|
||||||
topic: '',
|
title: '',
|
||||||
file: null,
|
file: null,
|
||||||
withCertificate: false,
|
certificate: true,
|
||||||
|
text: '',
|
||||||
|
total_price: 41200,
|
||||||
};
|
};
|
||||||
|
|
||||||
const INITIAL_SUBMISSION: SubmissionState = {
|
const INITIAL_SUBMISSION: SubmissionState = {
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
response: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Hook ────────────────────────────────────────────────────────────────────
|
// ─── Hook ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function usePlagiarismForm() {
|
export function usePlagiarismForm() {
|
||||||
const senderFullName = useUserStore(selectFullName);
|
|
||||||
const user = useUserPlagiatStore((state) => state.user);
|
const user = useUserPlagiatStore((state) => state.user);
|
||||||
const [form, setForm] = useState<PlagiarismFormState>(INITIAL_FORM);
|
const [form, setForm] = useState<PlagiarismFormState>(INITIAL_FORM);
|
||||||
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
|
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
|
||||||
@@ -36,15 +37,28 @@ export function usePlagiarismForm() {
|
|||||||
const [submission, setSubmission] =
|
const [submission, setSubmission] =
|
||||||
useState<SubmissionState>(INITIAL_SUBMISSION);
|
useState<SubmissionState>(INITIAL_SUBMISSION);
|
||||||
|
|
||||||
// const checkdocumentRequest = useMutation({
|
const checkdocumentRequest = useMutation({
|
||||||
// mutationFn: (data:any) => apiRequest("POST",links.plagiarismCheck, data)
|
mutationKey: ['plagiarismCheck'],
|
||||||
// })
|
mutationFn: (data: FormData) =>
|
||||||
|
apiRequest('POST', links.plagiarismCheck, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
setSubmission({ status: 'success', error: null });
|
||||||
|
setForm(INITIAL_FORM);
|
||||||
|
setIsPaymentOpen(false);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : 'An unexpected error occurred.';
|
||||||
|
setSubmission({ status: 'error', error: message });
|
||||||
|
setIsPaymentOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ── Field updaters ───────────────────────────────────────────────────────
|
// ── Field updaters ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const setTopic = useCallback((topic: string) => {
|
const setTopic = useCallback((topic: string) => {
|
||||||
setForm((prev) => ({ ...prev, topic }));
|
setForm((prev) => ({ ...prev, title: topic }));
|
||||||
setErrors((prev) => ({ ...prev, topic: undefined }));
|
setErrors((prev) => ({ ...prev, title: undefined }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setFile = useCallback((file: File | null) => {
|
const setFile = useCallback((file: File | null) => {
|
||||||
@@ -53,7 +67,7 @@ export function usePlagiarismForm() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleCertificate = useCallback(() => {
|
const toggleCertificate = useCallback(() => {
|
||||||
setForm((prev) => ({ ...prev, withCertificate: !prev.withCertificate }));
|
setForm((prev) => ({ ...prev, certificate: !prev.certificate }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── Submission ───────────────────────────────────────────────────────────
|
// ── Submission ───────────────────────────────────────────────────────────
|
||||||
@@ -82,23 +96,15 @@ export function usePlagiarismForm() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(async () => {
|
const handleSubmit = useCallback(async () => {
|
||||||
setSubmission({ status: 'loading', response: null, error: null });
|
setSubmission({ status: 'loading', error: null });
|
||||||
try {
|
const fd = new FormData();
|
||||||
const response = await submitPlagiarismCheck({
|
fd.append('title', form.title.trim());
|
||||||
topic: form.topic.trim(),
|
fd.append('text', `${user?.name} ${user?.surname}` || '');
|
||||||
senderFullName,
|
fd.append('file', form.file!); // File object — multipart/form-data
|
||||||
file: form.file!,
|
fd.append('certificate', String(form.certificate));
|
||||||
withCertificate: form.withCertificate,
|
fd.append('total_price', '41200');
|
||||||
});
|
checkdocumentRequest.mutate(fd);
|
||||||
setSubmission({ status: 'success', response, error: null });
|
}, [form, user]);
|
||||||
setForm(INITIAL_FORM);
|
|
||||||
setIsPaymentOpen(false); // Close modal on success
|
|
||||||
} catch (err) {
|
|
||||||
const message =
|
|
||||||
err instanceof Error ? err.message : 'An unexpected error occurred.';
|
|
||||||
setSubmission({ status: 'error', response: null, error: message });
|
|
||||||
}
|
|
||||||
}, [form, senderFullName]);
|
|
||||||
|
|
||||||
const resetSubmission = useCallback(() => {
|
const resetSubmission = useCallback(() => {
|
||||||
setSubmission(INITIAL_SUBMISSION);
|
setSubmission(INITIAL_SUBMISSION);
|
||||||
@@ -112,7 +118,7 @@ export function usePlagiarismForm() {
|
|||||||
form,
|
form,
|
||||||
errors,
|
errors,
|
||||||
submission,
|
submission,
|
||||||
senderFullName,
|
senderFullName: user ? `${user?.name} ${user?.surname}` : null,
|
||||||
isLoading,
|
isLoading,
|
||||||
setTopic,
|
setTopic,
|
||||||
setFile,
|
setFile,
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ export function validatePlagiarismForm(
|
|||||||
const errors: PlagiarismFormErrors = {};
|
const errors: PlagiarismFormErrors = {};
|
||||||
|
|
||||||
// Topic validation
|
// Topic validation
|
||||||
const trimmedTopic = state.topic.trim();
|
const trimmedTopic = state.title.trim();
|
||||||
if (!trimmedTopic) {
|
if (!trimmedTopic) {
|
||||||
errors.topic = 'Topic is required.';
|
errors.title = 'Title is required.';
|
||||||
} else if (trimmedTopic.length < 3) {
|
} else if (trimmedTopic.length < 3) {
|
||||||
errors.topic = 'Topic must be at least 3 characters.';
|
errors.title = 'Title must be at least 3 characters.';
|
||||||
} else if (trimmedTopic.length > 200) {
|
} else if (trimmedTopic.length > 200) {
|
||||||
errors.topic = 'Topic must not exceed 200 characters.';
|
errors.title = 'Title must not exceed 200 characters.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// File validation
|
// File validation
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ export function PlagiarismCheckForm() {
|
|||||||
className="p-7 flex md:flex-row flex-col gap-6"
|
className="p-7 flex md:flex-row flex-col gap-6"
|
||||||
>
|
>
|
||||||
{/* Status banners */}
|
{/* Status banners */}
|
||||||
{submission.status === 'success' && submission.response && (
|
{submission.status === 'success' && (
|
||||||
<StatusBanner
|
<StatusBanner
|
||||||
status="success"
|
status="success"
|
||||||
message={`Submission successful! ID: ${submission.response.submissionId}. ${submission.response.message}`}
|
message={`Submission successful! ID`}
|
||||||
onDismiss={resetSubmission}
|
onDismiss={resetSubmission}
|
||||||
dismissText={t('dismiss')}
|
dismissText={t('dismiss')}
|
||||||
/>
|
/>
|
||||||
@@ -105,17 +105,17 @@ export function PlagiarismCheckForm() {
|
|||||||
{/* Topic */}
|
{/* Topic */}
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
label={t('documentTopic')}
|
label={t('documentTopic')}
|
||||||
htmlFor="topic"
|
htmlFor="title"
|
||||||
error={errors.topic}
|
error={errors.title}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="topic"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('topicPlaceholder')}
|
placeholder={t('topicPlaceholder')}
|
||||||
value={form.topic}
|
value={form.title}
|
||||||
onChange={(e) => setTopic(e.target.value)}
|
onChange={(e) => setTopic(e.target.value)}
|
||||||
hasError={!!errors.topic}
|
hasError={!!errors.title}
|
||||||
maxLength={200}
|
maxLength={200}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
@@ -136,7 +136,7 @@ export function PlagiarismCheckForm() {
|
|||||||
{t('certificateOption')}
|
{t('certificateOption')}
|
||||||
</p>
|
</p>
|
||||||
<CertificateCheckbox
|
<CertificateCheckbox
|
||||||
checked={form.withCertificate}
|
checked={form.certificate}
|
||||||
onChange={toggleCertificate}
|
onChange={toggleCertificate}
|
||||||
title={t('certificateTitle')}
|
title={t('certificateTitle')}
|
||||||
description={t('certificateDescription')}
|
description={t('certificateDescription')}
|
||||||
@@ -186,7 +186,7 @@ export function PlagiarismCheckForm() {
|
|||||||
<PaymentModal
|
<PaymentModal
|
||||||
isOpen={isPaymentOpen}
|
isOpen={isPaymentOpen}
|
||||||
onClose={() => setIsPaymentOpen(false)}
|
onClose={() => setIsPaymentOpen(false)}
|
||||||
hasCertificate={form.withCertificate}
|
hasCertificate={form.certificate}
|
||||||
onConfirmPayment={handleSubmit}
|
onConfirmPayment={handleSubmit}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import { DEFAULT_PAGE_SIZE } from './constants';
|
import { DEFAULT_PAGE_SIZE } from './constants';
|
||||||
import { HistoryState } from './types';
|
import { HistoryState } from './types';
|
||||||
import { DEFAULT_HISTORY_ITEMS } from './mock';
|
import { DEFAULT_HISTORY_ITEMS } from './mock';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { links } from '@/shared/request/links';
|
||||||
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
|
|
||||||
interface UseHistoryReturn extends HistoryState {
|
interface UseHistoryReturn extends HistoryState {
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
@@ -20,27 +23,37 @@ export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
const loadHistory = useCallback(
|
const { data, refetch } = useQuery({
|
||||||
async (page: number) => {
|
queryKey: ['history'],
|
||||||
setState((prev) => ({ ...prev, status: 'loading', error: null }));
|
queryFn: () => apiRequest('GET', links.history),
|
||||||
setTotal(0);
|
select: (response) => {
|
||||||
console.log(page);
|
const { results, total } = response.data as {
|
||||||
|
results: [];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
return { results, total };
|
||||||
},
|
},
|
||||||
[pageSize],
|
});
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadHistory(currentPage);
|
if (data) {
|
||||||
}, [currentPage, loadHistory]);
|
setState({
|
||||||
|
items: data?.results || [],
|
||||||
|
status: 'success',
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
setTotal(data?.total || 0);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch();
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
const goToPage = useCallback((page: number) => {
|
const goToPage = useCallback((page: number) => {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refetch = useCallback(() => {
|
|
||||||
loadHistory(currentPage);
|
|
||||||
}, [currentPage, loadHistory]);
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / pageSize);
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,19 +4,11 @@ import { useTranslations } from 'next-intl';
|
|||||||
import { useHistory } from '../lib/useHistory';
|
import { useHistory } from '../lib/useHistory';
|
||||||
import { HistoryTable } from './historyTable';
|
import { HistoryTable } from './historyTable';
|
||||||
import { Pagination } from './pagination';
|
import { Pagination } from './pagination';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
|
||||||
import { links } from '@/shared/request/links';
|
|
||||||
|
|
||||||
// ─── Page Header ───────────────────────────────────────────────────────────────
|
// ─── Page Header ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PageHeader: React.FC = () => {
|
const PageHeader: React.FC = () => {
|
||||||
const t = useTranslations('HistoryPage');
|
const t = useTranslations('HistoryPage');
|
||||||
const { data } = useQuery({
|
|
||||||
queryKey: ['history'],
|
|
||||||
queryFn: () => apiRequest('GET', links.history),
|
|
||||||
});
|
|
||||||
console.log('History data:', data); // Debugging log
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { ChangeLang } from './ChangeLang';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AuthButtons } from './authButtons';
|
import { AuthButtons } from './authButtons';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { Logo_image } from '@/image';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const t = useTranslations('Navbar');
|
const t = useTranslations('Navbar');
|
||||||
@@ -23,16 +25,22 @@ const Navbar = () => {
|
|||||||
<section className="py-4 flex items-center justify-center w-full ">
|
<section className="py-4 flex items-center justify-center w-full ">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||||
{/* Desktop Menu */}
|
{/* Desktop Menu */}
|
||||||
<nav className="justify-between flex max-sm:flex-col gap-5">
|
<nav className="justify-between items-center flex max-sm:flex-col gap-5">
|
||||||
<div className="flex items-center justify-between gap-6">
|
<div className="flex items-center justify-between gap-6">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link
|
<Link
|
||||||
href={'/'}
|
href={'/'}
|
||||||
className="flex items-center gap-2 text-2xl font-bold "
|
className="flex items-center gap-2 text-2xl font-bold "
|
||||||
>
|
>
|
||||||
{t('logo')}
|
<Image
|
||||||
|
src={Logo_image}
|
||||||
|
className="min-h-4"
|
||||||
|
alt="Anti-Plagiat.uz"
|
||||||
|
width={200}
|
||||||
|
height={50}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex sm:hidden">
|
<div className="flex sm:hidden items-center justify-center">
|
||||||
<ChangeLang />
|
<ChangeLang />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,26 +51,25 @@ export const PriceSummary: React.FC<PriceSummaryProps> = ({
|
|||||||
hasCertificate,
|
hasCertificate,
|
||||||
pricing,
|
pricing,
|
||||||
}) => {
|
}) => {
|
||||||
const total = hasCertificate
|
console.log(hasCertificate);
|
||||||
? pricing.serviceFee + pricing.certificateFee
|
const total = 41200;
|
||||||
: pricing.serviceFee;
|
|
||||||
const t = useTranslations('Payment');
|
const t = useTranslations('Payment');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-slate-50 border border-slate-100 px-5 py-2 space-y-0">
|
<div className="rounded-xl bg-slate-50 border border-slate-100 px-5 py-2 space-y-0">
|
||||||
<PriceRow
|
<PriceRow
|
||||||
label={t('serviceFee')}
|
label={t('serviceFee')}
|
||||||
amount={pricing.serviceFee}
|
amount={41200}
|
||||||
currency={pricing.currency}
|
currency={pricing.currency}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasCertificate && (
|
{/* {hasCertificate && (
|
||||||
<PriceRow
|
<PriceRow
|
||||||
label={t('certificateLabel')}
|
label={t('certificateLabel')}
|
||||||
amount={pricing.certificateFee}
|
amount={pricing.certificateFee}
|
||||||
currency={pricing.currency}
|
currency={pricing.currency}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
<PriceRow
|
<PriceRow
|
||||||
label={t('total')}
|
label={t('total')}
|
||||||
|
|||||||
Reference in New Issue
Block a user