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 {
|
||||
params: Promise<{ detail: string }>;
|
||||
@@ -6,5 +6,6 @@ interface Props {
|
||||
|
||||
export default async function DetailPage({ params }: Props) {
|
||||
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 = {
|
||||
login: '/users/login/',
|
||||
register: '/users/register/',
|
||||
plagiarismCheck: '/plagiarism/check/',
|
||||
plagiarismCheck: '/shared/documents/',
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
export interface PlagiarismFormState {
|
||||
topic: string;
|
||||
title: string;
|
||||
file: File | null;
|
||||
withCertificate: boolean;
|
||||
certificate: boolean;
|
||||
text?: string;
|
||||
total_price: number;
|
||||
}
|
||||
|
||||
export type PlagiarismFormErrors = Partial<
|
||||
@@ -39,6 +41,5 @@ export type SubmissionStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
export interface SubmissionState {
|
||||
status: SubmissionStatus;
|
||||
response: PlagiarismSubmissionResponse | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@@ -5,30 +5,31 @@ import {
|
||||
PlagiarismFormState,
|
||||
SubmissionState,
|
||||
} from './types';
|
||||
import { selectFullName, useUserStore } from './userStore';
|
||||
import { isFormValid, validatePlagiarismForm } from './validation';
|
||||
import { submitPlagiarismCheck } from '@/shared/request/plagiarismapi';
|
||||
import { toast } from 'react-toastify';
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
|
||||
const INITIAL_FORM: PlagiarismFormState = {
|
||||
topic: '',
|
||||
title: '',
|
||||
file: null,
|
||||
withCertificate: false,
|
||||
certificate: true,
|
||||
text: '',
|
||||
total_price: 41200,
|
||||
};
|
||||
|
||||
const INITIAL_SUBMISSION: SubmissionState = {
|
||||
status: 'idle',
|
||||
response: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// ─── Hook ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function usePlagiarismForm() {
|
||||
const senderFullName = useUserStore(selectFullName);
|
||||
const user = useUserPlagiatStore((state) => state.user);
|
||||
const [form, setForm] = useState<PlagiarismFormState>(INITIAL_FORM);
|
||||
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
|
||||
@@ -36,15 +37,28 @@ export function usePlagiarismForm() {
|
||||
const [submission, setSubmission] =
|
||||
useState<SubmissionState>(INITIAL_SUBMISSION);
|
||||
|
||||
// const checkdocumentRequest = useMutation({
|
||||
// mutationFn: (data:any) => apiRequest("POST",links.plagiarismCheck, data)
|
||||
// })
|
||||
const checkdocumentRequest = useMutation({
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
||||
const setTopic = useCallback((topic: string) => {
|
||||
setForm((prev) => ({ ...prev, topic }));
|
||||
setErrors((prev) => ({ ...prev, topic: undefined }));
|
||||
setForm((prev) => ({ ...prev, title: topic }));
|
||||
setErrors((prev) => ({ ...prev, title: undefined }));
|
||||
}, []);
|
||||
|
||||
const setFile = useCallback((file: File | null) => {
|
||||
@@ -53,7 +67,7 @@ export function usePlagiarismForm() {
|
||||
}, []);
|
||||
|
||||
const toggleCertificate = useCallback(() => {
|
||||
setForm((prev) => ({ ...prev, withCertificate: !prev.withCertificate }));
|
||||
setForm((prev) => ({ ...prev, certificate: !prev.certificate }));
|
||||
}, []);
|
||||
|
||||
// ── Submission ───────────────────────────────────────────────────────────
|
||||
@@ -82,23 +96,15 @@ export function usePlagiarismForm() {
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
setSubmission({ status: 'loading', response: null, error: null });
|
||||
try {
|
||||
const response = await submitPlagiarismCheck({
|
||||
topic: form.topic.trim(),
|
||||
senderFullName,
|
||||
file: form.file!,
|
||||
withCertificate: form.withCertificate,
|
||||
});
|
||||
setSubmission({ status: 'success', response, error: null });
|
||||
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]);
|
||||
setSubmission({ status: 'loading', error: null });
|
||||
const fd = new FormData();
|
||||
fd.append('title', form.title.trim());
|
||||
fd.append('text', `${user?.name} ${user?.surname}` || '');
|
||||
fd.append('file', form.file!); // File object — multipart/form-data
|
||||
fd.append('certificate', String(form.certificate));
|
||||
fd.append('total_price', '41200');
|
||||
checkdocumentRequest.mutate(fd);
|
||||
}, [form, user]);
|
||||
|
||||
const resetSubmission = useCallback(() => {
|
||||
setSubmission(INITIAL_SUBMISSION);
|
||||
@@ -112,7 +118,7 @@ export function usePlagiarismForm() {
|
||||
form,
|
||||
errors,
|
||||
submission,
|
||||
senderFullName,
|
||||
senderFullName: user ? `${user?.name} ${user?.surname}` : null,
|
||||
isLoading,
|
||||
setTopic,
|
||||
setFile,
|
||||
|
||||
@@ -22,13 +22,13 @@ export function validatePlagiarismForm(
|
||||
const errors: PlagiarismFormErrors = {};
|
||||
|
||||
// Topic validation
|
||||
const trimmedTopic = state.topic.trim();
|
||||
const trimmedTopic = state.title.trim();
|
||||
if (!trimmedTopic) {
|
||||
errors.topic = 'Topic is required.';
|
||||
errors.title = 'Title is required.';
|
||||
} 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) {
|
||||
errors.topic = 'Topic must not exceed 200 characters.';
|
||||
errors.title = 'Title must not exceed 200 characters.';
|
||||
}
|
||||
|
||||
// File validation
|
||||
|
||||
@@ -83,10 +83,10 @@ export function PlagiarismCheckForm() {
|
||||
className="p-7 flex md:flex-row flex-col gap-6"
|
||||
>
|
||||
{/* Status banners */}
|
||||
{submission.status === 'success' && submission.response && (
|
||||
{submission.status === 'success' && (
|
||||
<StatusBanner
|
||||
status="success"
|
||||
message={`Submission successful! ID: ${submission.response.submissionId}. ${submission.response.message}`}
|
||||
message={`Submission successful! ID`}
|
||||
onDismiss={resetSubmission}
|
||||
dismissText={t('dismiss')}
|
||||
/>
|
||||
@@ -105,17 +105,17 @@ export function PlagiarismCheckForm() {
|
||||
{/* Topic */}
|
||||
<FieldWrapper
|
||||
label={t('documentTopic')}
|
||||
htmlFor="topic"
|
||||
error={errors.topic}
|
||||
htmlFor="title"
|
||||
error={errors.title}
|
||||
required
|
||||
>
|
||||
<TextInput
|
||||
id="topic"
|
||||
id="title"
|
||||
type="text"
|
||||
placeholder={t('topicPlaceholder')}
|
||||
value={form.topic}
|
||||
value={form.title}
|
||||
onChange={(e) => setTopic(e.target.value)}
|
||||
hasError={!!errors.topic}
|
||||
hasError={!!errors.title}
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -136,7 +136,7 @@ export function PlagiarismCheckForm() {
|
||||
{t('certificateOption')}
|
||||
</p>
|
||||
<CertificateCheckbox
|
||||
checked={form.withCertificate}
|
||||
checked={form.certificate}
|
||||
onChange={toggleCertificate}
|
||||
title={t('certificateTitle')}
|
||||
description={t('certificateDescription')}
|
||||
@@ -186,7 +186,7 @@ export function PlagiarismCheckForm() {
|
||||
<PaymentModal
|
||||
isOpen={isPaymentOpen}
|
||||
onClose={() => setIsPaymentOpen(false)}
|
||||
hasCertificate={form.withCertificate}
|
||||
hasCertificate={form.certificate}
|
||||
onConfirmPayment={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,9 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { DEFAULT_PAGE_SIZE } from './constants';
|
||||
import { HistoryState } from './types';
|
||||
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 {
|
||||
refetch: () => void;
|
||||
@@ -20,27 +23,37 @@ export const useHistory = (pageSize = DEFAULT_PAGE_SIZE): UseHistoryReturn => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const loadHistory = useCallback(
|
||||
async (page: number) => {
|
||||
setState((prev) => ({ ...prev, status: 'loading', error: null }));
|
||||
setTotal(0);
|
||||
console.log(page);
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['history'],
|
||||
queryFn: () => apiRequest('GET', links.history),
|
||||
select: (response) => {
|
||||
const { results, total } = response.data as {
|
||||
results: [];
|
||||
total: number;
|
||||
};
|
||||
return { results, total };
|
||||
},
|
||||
[pageSize],
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadHistory(currentPage);
|
||||
}, [currentPage, loadHistory]);
|
||||
if (data) {
|
||||
setState({
|
||||
items: data?.results || [],
|
||||
status: 'success',
|
||||
error: null,
|
||||
});
|
||||
setTotal(data?.total || 0);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [currentPage]);
|
||||
|
||||
const goToPage = useCallback((page: number) => {
|
||||
setCurrentPage(page);
|
||||
}, []);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
loadHistory(currentPage);
|
||||
}, [currentPage, loadHistory]);
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,19 +4,11 @@ import { useTranslations } from 'next-intl';
|
||||
import { useHistory } from '../lib/useHistory';
|
||||
import { HistoryTable } from './historyTable';
|
||||
import { Pagination } from './pagination';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiRequest } from '@/shared/request/apiRequest';
|
||||
import { links } from '@/shared/request/links';
|
||||
|
||||
// ─── Page Header ───────────────────────────────────────────────────────────────
|
||||
|
||||
const PageHeader: React.FC = () => {
|
||||
const t = useTranslations('HistoryPage');
|
||||
const { data } = useQuery({
|
||||
queryKey: ['history'],
|
||||
queryFn: () => apiRequest('GET', links.history),
|
||||
});
|
||||
console.log('History data:', data); // Debugging log
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
|
||||
@@ -14,6 +14,8 @@ import { ChangeLang } from './ChangeLang';
|
||||
import Link from 'next/link';
|
||||
import { AuthButtons } from './authButtons';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Logo_image } from '@/image';
|
||||
import Image from 'next/image';
|
||||
|
||||
const Navbar = () => {
|
||||
const t = useTranslations('Navbar');
|
||||
@@ -23,16 +25,22 @@ const Navbar = () => {
|
||||
<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">
|
||||
{/* 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">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href={'/'}
|
||||
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>
|
||||
<div className="flex sm:hidden">
|
||||
<div className="flex sm:hidden items-center justify-center">
|
||||
<ChangeLang />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,26 +51,25 @@ export const PriceSummary: React.FC<PriceSummaryProps> = ({
|
||||
hasCertificate,
|
||||
pricing,
|
||||
}) => {
|
||||
const total = hasCertificate
|
||||
? pricing.serviceFee + pricing.certificateFee
|
||||
: pricing.serviceFee;
|
||||
console.log(hasCertificate);
|
||||
const total = 41200;
|
||||
const t = useTranslations('Payment');
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-slate-50 border border-slate-100 px-5 py-2 space-y-0">
|
||||
<PriceRow
|
||||
label={t('serviceFee')}
|
||||
amount={pricing.serviceFee}
|
||||
amount={41200}
|
||||
currency={pricing.currency}
|
||||
/>
|
||||
|
||||
{hasCertificate && (
|
||||
{/* {hasCertificate && (
|
||||
<PriceRow
|
||||
label={t('certificateLabel')}
|
||||
amount={pricing.certificateFee}
|
||||
currency={pricing.currency}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<PriceRow
|
||||
label={t('total')}
|
||||
|
||||
Reference in New Issue
Block a user