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)}
require={true}
type="password"
maxLength={8}
minLength={8}
/>
</div>

View File

@@ -6,14 +6,14 @@ import {
User,
FileText,
BookOpen,
Layers,
Loader2,
CheckCircle2,
} from 'lucide-react';
import { useCertificateModal } from './useSertificateModal';
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({
document_id,
@@ -138,35 +138,11 @@ export default function SertificateModal({
</Field>
{/* Document type */}
<Field
htmlFor="document_type"
icon={
<Layers className="w-3.5 h-3.5 text-slate-400" strokeWidth={2} />
}
label="Hujjat turi"
>
<select
id="document_type"
value={form.document_type}
onChange={(e) =>
updateField(
'document_type',
e.target.value as typeof form.document_type,
)
}
<DocumentsTypes
value={String(form.document_type)}
onChange={(val) => updateField('document_type', val)}
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) */}
<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 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 }) {
const t = useTranslations();
const [loading, setLoading] = useState(false);
@@ -16,26 +13,6 @@ export default function Sertifikat({ document_id }: { document_id: number }) {
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 (
<>
<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 {
fullname: string;
document_theme: string;
document_type: DocumentTypeValue | '';
document_type: string | 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';
interface UseCertificateModalProps {
@@ -7,6 +10,12 @@ interface UseCertificateModalProps {
setOpen: () => void;
}
interface CertificatePayload {
full_name: string;
file_name: string;
document_type: number;
}
export function useCertificateModal({
document_id,
open,
@@ -18,11 +27,43 @@ export function useCertificateModal({
document_type: '',
document_id,
});
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [visible, setVisible] = useState(false);
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(() => {
if (open) {
setVisible(true);
@@ -53,39 +94,13 @@ export function useCertificateModal({
!!form.document_theme.trim() &&
!!form.document_type;
/** Payload ready to send to backend */
const buildPayload = (): CertificateFormData => ({ ...form });
const handleSubmit = async () => {
if (!isFormValid || loading) return;
setLoading(true);
try {
const payload = buildPayload();
const response = await fetch(`/api/certificates`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
const handleSubmit = () => {
if (!isFormValid || certificateMutation.isPending) return;
certificateMutation.mutate({
full_name: form.fullname,
file_name: form.document_theme,
document_type: Number(form.document_type),
});
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) => {
@@ -100,7 +115,7 @@ export function useCertificateModal({
return {
form,
updateField,
loading,
loading: certificateMutation.isPending,
success,
visible,
isFormValid,

View File

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

View File

@@ -1,35 +1,76 @@
'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';
interface ProfileForm extends UserProfile {
currentPassword: string;
newPassword: string;
confirmPassword: string;
interface ProfileFormState {
first_name: string;
last_name: string;
phone: string;
password: string;
}
export const useProfile = (initial: UserProfile) => {
const [form, setForm] = useState<ProfileForm>({
...initial,
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [isSaving, setIsSaving] = useState(false);
export const useProfile = () => {
const queryClient = useQueryClient();
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 }));
setSaved(false);
};
const handleSave = async () => {
setIsSaving(true);
// TODO: replace with real API call
await new Promise((res) => setTimeout(res, 800));
setIsSaving(false);
setSaved(true);
const handleSave = () => {
const payload: Record<string, string> = {
first_name: form.first_name,
last_name: form.last_name,
};
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';
export const MOCK_USER: UserProfile = {
name: 'Ali',
surname: 'Karimov',
first_name: 'Ali',
last_name: 'Karimov',
email: 'ali.karimov@gmail.com',
phone: '+998 90 123 45 67',
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,8 +21,7 @@ const INITIAL_FORM: PlagiarismFormState = {
file: null,
certificate: true,
text: '',
total_price: 41200,
document_type: 'boshqa',
type: 'boshqa',
};
const PRICE: PriceCalculate = {
@@ -145,16 +144,17 @@ export function usePlagiarismForm() {
return; // Don't open modal if invalid
}
console.log('new');
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('text', form.text || '');
fd.append('file', form.file!);
fd.append('certificate', String(form.certificate));
fd.append('total_price', '41200');
fd.append('type', form.document_type);
fd.append('type', form.type);
console.log('sended data: ', fd);
checkdocumentRequest.mutate(fd);
},
[form],
[form, localUser],
);
const handleSubmit = useCallback(async () => {

View File

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