profile page connected to backend and PATCH added, plagiatCheck updated and sertificate generate updated base backend types

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-07 21:30:27 +05:00
parent 50a8d6dbd7
commit c61182adcf
15 changed files with 208 additions and 198 deletions

View File

@@ -122,7 +122,6 @@ export function LoginForm() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
require={true} require={true}
type="password" type="password"
maxLength={8}
minLength={8} minLength={8}
/> />
</div> </div>

View File

@@ -6,14 +6,14 @@ import {
User, User,
FileText, FileText,
BookOpen, BookOpen,
Layers,
Loader2, Loader2,
CheckCircle2, CheckCircle2,
} from 'lucide-react'; } from 'lucide-react';
import { useCertificateModal } from './useSertificateModal'; import { useCertificateModal } from './useSertificateModal';
import { Field, inputCls } from './modalField'; import { Field, inputCls } from './modalField';
import { DOCUMENT_TYPES, SertificateModalProps } from './types'; import { SertificateModalProps } from './types';
import DocumentsTypes from '@/widgets/plagiatCheck/ui/documentsType';
export default function SertificateModal({ export default function SertificateModal({
document_id, document_id,
@@ -138,35 +138,11 @@ export default function SertificateModal({
</Field> </Field>
{/* Document type */} {/* Document type */}
<Field <DocumentsTypes
htmlFor="document_type" value={String(form.document_type)}
icon={ onChange={(val) => updateField('document_type', val)}
<Layers className="w-3.5 h-3.5 text-slate-400" strokeWidth={2} /> disabled={loading || success}
} />
label="Hujjat turi"
>
<select
id="document_type"
value={form.document_type}
onChange={(e) =>
updateField(
'document_type',
e.target.value as typeof form.document_type,
)
}
disabled={loading || success}
className={`${inputCls} cursor-pointer`}
>
<option value="" disabled>
Hujjat turini tanlang...
</option>
{DOCUMENT_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</Field>
{/* Document ID (read-only) */} {/* Document ID (read-only) */}
<div className="flex items-center gap-2 px-3.5 py-2.5 rounded-xl bg-slate-50 border border-slate-100"> <div className="flex items-center gap-2 px-3.5 py-2.5 rounded-xl bg-slate-50 border border-slate-100">

View File

@@ -4,9 +4,6 @@ import { FileDown, Loader2 } from 'lucide-react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import SertificateModal from './sertificateModal'; import SertificateModal from './sertificateModal';
// const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
// const baseUrl = 'https://api.anti-plagiat.uz/api/v1';
export default function Sertifikat({ document_id }: { document_id: number }) { export default function Sertifikat({ document_id }: { document_id: number }) {
const t = useTranslations(); const t = useTranslations();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -16,26 +13,6 @@ export default function Sertifikat({ document_id }: { document_id: number }) {
console.log(loading); console.log(loading);
}, []); }, []);
// const handleClick = async () => {
// setLoading(true);
// try {
// const url = `${baseUrl}/shared/certificate/${document_id}/pdf/`;
// const res = await fetch(url);
// const blob = await res.blob();
// const objectUrl = URL.createObjectURL(blob);
// // ✅ window.open o'rniga <a> tag bilan download
// const a = document.createElement('a');
// a.href = objectUrl;
// a.download = `certificate-${document_id}.pdf`;
// a.click();
// URL.revokeObjectURL(objectUrl);
// } finally {
// setLoading(false);
// }
// };
return ( return (
<> <>
<button <button

View File

@@ -1,18 +1,7 @@
export const DOCUMENT_TYPES = [
{ value: 'metodik_ishlanma', label: 'Metodik ishlanma' },
{ value: 'ilmiy_maqola', label: 'Ilmiy maqola' },
{ value: 'bmi', label: 'BMI' },
{ value: 'magistrlik', label: 'Magistrlik' },
{ value: 'kurs_ishi', label: 'Kurs ishi' },
{ value: 'boshqa', label: 'Boshqa' },
] as const;
export type DocumentTypeValue = (typeof DOCUMENT_TYPES)[number]['value'];
export interface CertificateFormData { export interface CertificateFormData {
fullname: string; fullname: string;
document_theme: string; document_theme: string;
document_type: DocumentTypeValue | ''; document_type: string | number;
document_id: number; document_id: number;
} }

View File

@@ -1,4 +1,7 @@
import { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useMutation } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import { CertificateFormData } from './types'; import { CertificateFormData } from './types';
interface UseCertificateModalProps { interface UseCertificateModalProps {
@@ -7,6 +10,12 @@ interface UseCertificateModalProps {
setOpen: () => void; setOpen: () => void;
} }
interface CertificatePayload {
full_name: string;
file_name: string;
document_type: number;
}
export function useCertificateModal({ export function useCertificateModal({
document_id, document_id,
open, open,
@@ -18,11 +27,43 @@ export function useCertificateModal({
document_type: '', document_type: '',
document_id, document_id,
}); });
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const certificateMutation = useMutation({
mutationFn: (payload: CertificatePayload) =>
apiRequest('POST', links.sertifikat(document_id), payload).then(
(res) => res.data as ArrayBuffer,
),
onSuccess: (data: ArrayBuffer) => {
if (data) {
const blob = new Blob([data], { type: 'application/pdf' });
const objectUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objectUrl;
a.download = `certificate-${document_id}.pdf`;
a.click();
URL.revokeObjectURL(objectUrl);
}
setSuccess(true);
setTimeout(() => {
setOpen();
setSuccess(false);
resetForm();
}, 1500);
},
});
const resetForm = () => {
setForm({
fullname: '',
document_theme: '',
document_type: '',
document_id,
});
};
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setVisible(true); setVisible(true);
@@ -53,39 +94,13 @@ export function useCertificateModal({
!!form.document_theme.trim() && !!form.document_theme.trim() &&
!!form.document_type; !!form.document_type;
/** Payload ready to send to backend */ const handleSubmit = () => {
const buildPayload = (): CertificateFormData => ({ ...form }); if (!isFormValid || certificateMutation.isPending) return;
certificateMutation.mutate({
const handleSubmit = async () => { full_name: form.fullname,
if (!isFormValid || loading) return; file_name: form.document_theme,
setLoading(true); document_type: Number(form.document_type),
});
try {
const payload = buildPayload();
const response = await fetch(`/api/certificates`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error('Failed');
setSuccess(true);
setTimeout(() => {
setOpen();
setSuccess(false);
}, 1800);
} catch {
// Demo mode: simulate success
setSuccess(true);
setTimeout(() => {
setOpen();
setSuccess(false);
}, 1800);
} finally {
setLoading(false);
}
}; };
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -100,7 +115,7 @@ export function useCertificateModal({
return { return {
form, form,
updateField, updateField,
loading, loading: certificateMutation.isPending,
success, success,
visible, visible,
isFormValid, isFormValid,

View File

@@ -178,7 +178,7 @@ api.interceptors.response.use(
// ─── Public request function ─────────────────────────────────────────────────── // ─── Public request function ───────────────────────────────────────────────────
export const apiRequest = async <T>( export const apiRequest = async <T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE', method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
url: string, url: string,
data?: unknown, data?: unknown,
config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>, config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>,

View File

@@ -1,35 +1,76 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import type { UserProfile } from '../types'; import type { UserProfile } from '../types';
interface ProfileForm extends UserProfile { interface ProfileFormState {
currentPassword: string; first_name: string;
newPassword: string; last_name: string;
confirmPassword: string; phone: string;
password: string;
} }
export const useProfile = (initial: UserProfile) => { export const useProfile = () => {
const [form, setForm] = useState<ProfileForm>({ const queryClient = useQueryClient();
...initial,
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [isSaving, setIsSaving] = useState(false);
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
const [form, setForm] = useState<ProfileFormState>({
first_name: '',
last_name: '',
phone: '',
password: '',
});
const handleChange = (field: keyof ProfileForm, value: string) => { const { data: profile, isLoading } = useQuery({
queryKey: ['profile'],
queryFn: () => apiRequest<UserProfile>('GET', links.users),
select: (res) => res.data,
});
useEffect(() => {
if (profile) {
setForm({
first_name: profile.first_name,
last_name: profile.last_name,
phone: profile.phone,
password: '',
});
}
}, [profile]);
const { mutate, isPending: isSaving } = useMutation({
mutationFn: (payload: Record<string, string>) =>
apiRequest<UserProfile>('PATCH', links.users, payload),
onSuccess: () => {
setSaved(true);
queryClient.invalidateQueries({ queryKey: ['profile'] });
setTimeout(() => setSaved(false), 3000);
},
});
const handleChange = (field: keyof ProfileFormState, value: string) => {
setForm((prev) => ({ ...prev, [field]: value })); setForm((prev) => ({ ...prev, [field]: value }));
setSaved(false); setSaved(false);
}; };
const handleSave = async () => { const handleSave = () => {
setIsSaving(true); const payload: Record<string, string> = {
// TODO: replace with real API call first_name: form.first_name,
await new Promise((res) => setTimeout(res, 800)); last_name: form.last_name,
setIsSaving(false); };
setSaved(true); if (form.phone) payload.phone = form.phone;
if (form.password) payload.password = form.password;
mutate(payload);
}; };
return { form, isSaving, saved, handleChange, handleSave }; return {
form,
profile,
isLoading,
isSaving,
saved,
handleChange,
handleSave,
};
}; };

View File

@@ -7,8 +7,8 @@ import type {
} from './types'; } from './types';
export const MOCK_USER: UserProfile = { export const MOCK_USER: UserProfile = {
name: 'Ali', first_name: 'Ali',
surname: 'Karimov', last_name: 'Karimov',
email: 'ali.karimov@gmail.com', email: 'ali.karimov@gmail.com',
phone: '+998 90 123 45 67', phone: '+998 90 123 45 67',
}; };

View File

@@ -10,8 +10,8 @@ export type CabinetSection =
// ─── Domain ──────────────────────────────────────────────────────────────────── // ─── Domain ────────────────────────────────────────────────────────────────────
export interface UserProfile { export interface UserProfile {
name: string; first_name: string;
surname: string; last_name: string;
email: string; email: string;
phone: string; phone: string;
} }

View File

@@ -45,7 +45,7 @@ const ProfileSection = dynamic(
function SectionContent({ section }: { section: CabinetSection }) { function SectionContent({ section }: { section: CabinetSection }) {
switch (section) { switch (section) {
case 'dashboard': case 'dashboard':
return <Dashboard userName={MOCK_USER.name} />; return <Dashboard userName={MOCK_USER.first_name} />;
case 'plagiat': case 'plagiat':
return <PlagiatTable />; return <PlagiatTable />;
case 'si': case 'si':
@@ -53,7 +53,7 @@ function SectionContent({ section }: { section: CabinetSection }) {
case 'payments': case 'payments':
return <PaymentsTable />; return <PaymentsTable />;
case 'profile': case 'profile':
return <ProfileSection user={MOCK_USER} stats={MOCK_STATS} />; return <ProfileSection stats={MOCK_STATS} />;
} }
} }
@@ -71,7 +71,7 @@ const FADE = {
export const CabinetLayout: React.FC = () => { export const CabinetLayout: React.FC = () => {
const { activeSection, navigate, isSidebarOpen, toggleSidebar } = const { activeSection, navigate, isSidebarOpen, toggleSidebar } =
useCabinet(); useCabinet();
const fullName = `${MOCK_USER.name} ${MOCK_USER.surname}`; const fullName = `${MOCK_USER.first_name} ${MOCK_USER.last_name}`;
return ( return (
<div className="flex bg-slate-50 min-h-screen"> <div className="flex bg-slate-50 min-h-screen">

View File

@@ -2,7 +2,6 @@
import React from 'react'; import React from 'react';
import { User, Phone, Lock, Save, CheckCircle } from 'lucide-react'; import { User, Phone, Lock, Save, CheckCircle } from 'lucide-react';
import { useProfile } from '../../lib/hooks/useProfile'; import { useProfile } from '../../lib/hooks/useProfile';
import type { UserProfile } from '../../lib/types';
// ─── Input field ─────────────────────────────────────────────────────────────── // ─── Input field ───────────────────────────────────────────────────────────────
@@ -13,6 +12,7 @@ interface InputFieldProps {
type?: string; type?: string;
icon: React.ElementType; icon: React.ElementType;
placeholder?: string; placeholder?: string;
disabled?: boolean;
} }
const InputField: React.FC<InputFieldProps> = ({ const InputField: React.FC<InputFieldProps> = ({
@@ -22,6 +22,7 @@ const InputField: React.FC<InputFieldProps> = ({
type = 'text', type = 'text',
icon: Icon, icon: Icon,
placeholder, placeholder,
disabled,
}) => ( }) => (
<div> <div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5"> <label className="block text-xs font-semibold text-slate-600 mb-1.5">
@@ -37,11 +38,13 @@ const InputField: React.FC<InputFieldProps> = ({
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder={placeholder} placeholder={placeholder}
disabled={disabled}
className=" className="
w-full pl-9 pr-4 py-2.5 text-sm rounded-xl w-full pl-9 pr-4 py-2.5 text-sm rounded-xl
border border-slate-200 bg-white border border-slate-200 bg-white
text-slate-800 placeholder:text-slate-300 text-slate-800 placeholder:text-slate-300
focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400
disabled:bg-slate-50 disabled:text-slate-400 disabled:cursor-not-allowed
transition-all duration-150 transition-all duration-150
" "
/> />
@@ -49,15 +52,27 @@ const InputField: React.FC<InputFieldProps> = ({
</div> </div>
); );
// ─── Skeleton ──────────────────────────────────────────────────────────────────
const FieldSkeleton = () => (
<div className="space-y-1.5">
<div className="h-3.5 w-20 bg-slate-200 rounded animate-pulse" />
<div className="h-10 w-full bg-slate-100 rounded-xl animate-pulse" />
</div>
);
// ─── Form ────────────────────────────────────────────────────────────────────── // ─── Form ──────────────────────────────────────────────────────────────────────
interface ProfileFormProps { export const ProfileForm: React.FC = () => {
initial: UserProfile; const {
} form,
profile,
export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => { isLoading,
const { form, isSaving, saved, handleChange, handleSave } = isSaving,
useProfile(initial); saved,
handleChange,
handleSave,
} = useProfile();
return ( return (
<div className="space-y-5"> <div className="space-y-5">
@@ -67,27 +82,46 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => {
Shaxsiy ma&apos;lumotlar Shaxsiy ma&apos;lumotlar
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InputField {isLoading ? (
label="Ism" <>
value={form.name} <FieldSkeleton />
onChange={(v) => handleChange('name', v)} <FieldSkeleton />
icon={User} <FieldSkeleton />
placeholder="Ali" <FieldSkeleton />
/> </>
<InputField ) : (
label="Familiya" <>
value={form.surname} <InputField
onChange={(v) => handleChange('surname', v)} label="Ism"
icon={User} value={form.first_name}
placeholder="Karimov" onChange={(v) => handleChange('first_name', v)}
/> icon={User}
<InputField placeholder="Ali"
label="Telefon" />
value={form.phone} <InputField
onChange={(v) => handleChange('phone', v)} label="Familiya"
icon={Phone} value={form.last_name}
placeholder="+998 90 123 45 67" onChange={(v) => handleChange('last_name', v)}
/> icon={User}
placeholder="Karimov"
/>
<InputField
label="Telefon"
value={form.phone}
onChange={(v) => handleChange('phone', v)}
icon={Phone}
placeholder="+998 90 123 45 67"
/>
<InputField
label="Email"
value={profile?.email ?? ''}
onChange={() => {}}
icon={User}
placeholder="email@example.com"
disabled
/>
</>
)}
</div> </div>
</div> </div>
@@ -96,27 +130,11 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => {
<h3 className="text-sm font-semibold text-slate-800 mb-4"> <h3 className="text-sm font-semibold text-slate-800 mb-4">
Parol o&apos;zgartirish Parol o&apos;zgartirish
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InputField
label="Joriy parol"
value={form.currentPassword}
onChange={(v) => handleChange('currentPassword', v)}
type="password"
icon={Lock}
placeholder="••••••••"
/>
<InputField <InputField
label="Yangi parol" label="Yangi parol"
value={form.newPassword} value={form.password}
onChange={(v) => handleChange('newPassword', v)} onChange={(v) => handleChange('password', v)}
type="password"
icon={Lock}
placeholder="••••••••"
/>
<InputField
label="Tasdiqlash"
value={form.confirmPassword}
onChange={(v) => handleChange('confirmPassword', v)}
type="password" type="password"
icon={Lock} icon={Lock}
placeholder="••••••••" placeholder="••••••••"
@@ -128,7 +146,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => {
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
onClick={handleSave} onClick={handleSave}
disabled={isSaving} disabled={isSaving || isLoading}
className={` className={`
flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-semibold text-white flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-semibold text-white
transition-all duration-200 transition-all duration-200

View File

@@ -1,17 +1,13 @@
import React from 'react'; import React from 'react';
import { DiscountProgress } from './DiscountProgress'; import { DiscountProgress } from './DiscountProgress';
import { ProfileForm } from './ProfileForm'; import { ProfileForm } from './ProfileForm';
import type { CabinetStats, UserProfile } from '../../lib/types'; import type { CabinetStats } from '../../lib/types';
interface ProfileSectionProps { interface ProfileSectionProps {
user: UserProfile;
stats: CabinetStats; stats: CabinetStats;
} }
export const ProfileSection: React.FC<ProfileSectionProps> = ({ export const ProfileSection: React.FC<ProfileSectionProps> = ({ stats }) => (
user,
stats,
}) => (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h2 className="text-xl font-bold text-slate-900">Profil</h2> <h2 className="text-xl font-bold text-slate-900">Profil</h2>
@@ -21,6 +17,6 @@ export const ProfileSection: React.FC<ProfileSectionProps> = ({
</div> </div>
<DiscountProgress stats={stats} /> <DiscountProgress stats={stats} />
<ProfileForm initial={user} /> <ProfileForm />
</div> </div>
); );

View File

@@ -35,8 +35,7 @@ export interface PlagiarismFormState {
file: File | null; file: File | null;
certificate: boolean; certificate: boolean;
text?: string; text?: string;
total_price: number; type: string;
document_type: string;
} }
export type PlagiarismFormErrors = Partial< export type PlagiarismFormErrors = Partial<

View File

@@ -21,8 +21,7 @@ const INITIAL_FORM: PlagiarismFormState = {
file: null, file: null,
certificate: true, certificate: true,
text: '', text: '',
total_price: 41200, type: 'boshqa',
document_type: 'boshqa',
}; };
const PRICE: PriceCalculate = { const PRICE: PriceCalculate = {
@@ -145,16 +144,17 @@ export function usePlagiarismForm() {
return; // Don't open modal if invalid return; // Don't open modal if invalid
} }
console.log('new');
const fd = new FormData(); const fd = new FormData();
fd.append('title', form.title.trim()); fd.append('title', form.title.trim());
fd.append('text', `${user?.name} ${user?.surname}` || ''); fd.append('text', form.text || '');
fd.append('file', form.file!); // File object — multipart/form-data fd.append('file', form.file!);
fd.append('certificate', String(form.certificate)); fd.append('certificate', String(form.certificate));
fd.append('total_price', '41200'); fd.append('type', form.type);
fd.append('type', form.document_type); console.log('sended data: ', fd);
checkdocumentRequest.mutate(fd); checkdocumentRequest.mutate(fd);
}, },
[form], [form, localUser],
); );
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(async () => {

View File

@@ -179,7 +179,7 @@ export function PlagiarismCheckForm() {
{/* Document type */} {/* Document type */}
<DocumentsTypes <DocumentsTypes
value={form.document_type} value={form.type}
onChange={setOption} onChange={setOption}
disabled={submission.status === 'success'} disabled={submission.status === 'success'}
/> />