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

@@ -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,27 +82,46 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ initial }) => {
Shaxsiy ma&apos;lumotlar
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InputField
label="Ism"
value={form.name}
onChange={(v) => handleChange('name', v)}
icon={User}
placeholder="Ali"
/>
<InputField
label="Familiya"
value={form.surname}
onChange={(v) => handleChange('surname', v)}
icon={User}
placeholder="Karimov"
/>
<InputField
label="Telefon"
value={form.phone}
onChange={(v) => handleChange('phone', v)}
icon={Phone}
placeholder="+998 90 123 45 67"
/>
{isLoading ? (
<>
<FieldSkeleton />
<FieldSkeleton />
<FieldSkeleton />
<FieldSkeleton />
</>
) : (
<>
<InputField
label="Ism"
value={form.first_name}
onChange={(v) => handleChange('first_name', v)}
icon={User}
placeholder="Ali"
/>
<InputField
label="Familiya"
value={form.last_name}
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>
@@ -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'}
/>