-
- {phone.length > 0 &&
- t('auth.entered_digits', { count: phone.length })}
-
+ {/* Digit counter / complete hint */}
+
+
+ {phone.length > 0 && `${phone.length} ta raqam kiritildi`}
+
{phone.length === 9 && (
-
- ✓ {t('auth.full')}
+
+ ✓ To‘liq
)}
+
{error}
)}
-
+
- {/* DIVIDER */}
-
-
-
-
-
-
- Ro‘yhatdan o‘tish
-
-
+ {/* Divider */}
+
+
+
+ yoki
+
+
+
+ {/* Register hint */}
+
+ Hisobingiz yo‘qmi?
+
+ Ro‘yxatdan o‘tish
+
+
diff --git a/src/features/auth/login/ui/index.ts b/src/features/auth/login/ui/index.ts
index f905d91..8d518e4 100644
--- a/src/features/auth/login/ui/index.ts
+++ b/src/features/auth/login/ui/index.ts
@@ -1,2 +1,2 @@
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
-export { MotionWrapper } from './motion';
+export { LoginForm } from './form';
diff --git a/src/features/auth/model.tsx b/src/features/auth/model.tsx
new file mode 100644
index 0000000..cbdb802
--- /dev/null
+++ b/src/features/auth/model.tsx
@@ -0,0 +1,21 @@
+'use client';
+import { AnimatePresence } from 'framer-motion';
+import { LoginForm } from './login/ui';
+import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
+import { RegisterForm } from './register/ui';
+function AuthModals() {
+ const openLoginModal = useLoginModal((state) => state.openLoginModal);
+ const openRegisterModal = useRegisterModal(
+ (state) => state.openRegisterModal,
+ );
+ return (
+
+
+ {openLoginModal && }{' '}
+ {openRegisterModal && }
+
+
+ );
+}
+
+export { AuthModals };
diff --git a/src/features/auth/register/lib/registerZustand.ts b/src/features/auth/register/lib/registerZustand.ts
new file mode 100644
index 0000000..d9dbe9c
--- /dev/null
+++ b/src/features/auth/register/lib/registerZustand.ts
@@ -0,0 +1,41 @@
+import { create } from 'zustand';
+
+export interface Registertype {
+ name: string;
+ surname: string;
+ phone: string;
+ oferta?: boolean;
+}
+
+interface RegisterZustandType {
+ registerData: Registertype;
+ setItem: (key: keyof Registertype, value: string) => void;
+ setOferta: (value: boolean) => void;
+ setRegisterData: (data: Registertype) => void;
+ clearRegisterData: () => void;
+}
+
+const INITIAL: Registertype = {
+ name: '',
+ surname: '',
+ phone: '',
+ oferta: false,
+};
+
+export const useRegisterZustand = create
((set) => ({
+ registerData: INITIAL,
+
+ setItem: (key, value) =>
+ set((state) => ({
+ registerData: { ...state.registerData, [key]: value },
+ })),
+
+ setOferta: (value) =>
+ set((state) => ({
+ registerData: { ...state.registerData, oferta: value },
+ })),
+
+ setRegisterData: (data) => set({ registerData: data }),
+
+ clearRegisterData: () => set({ registerData: INITIAL }),
+}));
diff --git a/src/features/auth/register/lib/useRegisterForm.ts b/src/features/auth/register/lib/useRegisterForm.ts
new file mode 100644
index 0000000..9a3a3c3
--- /dev/null
+++ b/src/features/auth/register/lib/useRegisterForm.ts
@@ -0,0 +1,64 @@
+import { useCallback, useState } from 'react';
+import { useRegisterZustand } from './registerZustand';
+import { validateRegister, RegisterErrors } from './validateRegister';
+
+export function useRegisterForm() {
+ const { registerData, setItem, setOferta, clearRegisterData } =
+ useRegisterZustand();
+ const [errors, setErrors] = useState({});
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setItem(name as keyof typeof registerData, value);
+ // clear the error for this field on change
+ setErrors((prev) => ({ ...prev, [name]: undefined }));
+ },
+ [setItem],
+ );
+
+ const handleOferta = useCallback(
+ (e: React.ChangeEvent) => {
+ setOferta(e.target.checked);
+ setErrors((prev) => ({ ...prev, oferta: undefined }));
+ },
+ [setOferta],
+ );
+
+ const handleSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+ const validationErrors = validateRegister(registerData);
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ // Replace with your real API call:
+ // await api.post('/register', registerData);
+ await new Promise((r) => setTimeout(r, 1200)); // simulated delay
+ setSuccess(true);
+ clearRegisterData();
+ } catch {
+ setErrors({ name: 'Something went wrong. Please try again.' });
+ } finally {
+ setLoading(false);
+ }
+ },
+ [registerData, clearRegisterData],
+ );
+
+ return {
+ registerData,
+ errors,
+ loading,
+ success,
+ handleChange,
+ handleOferta,
+ handleSubmit,
+ };
+}
diff --git a/src/features/auth/register/lib/validateRegister.ts b/src/features/auth/register/lib/validateRegister.ts
new file mode 100644
index 0000000..898b29a
--- /dev/null
+++ b/src/features/auth/register/lib/validateRegister.ts
@@ -0,0 +1,40 @@
+export interface RegisterErrors {
+ name?: string;
+ surname?: string;
+ phone?: string;
+ oferta?: string;
+}
+
+export function validateRegister(data: {
+ name: string;
+ surname: string;
+ phone: string;
+ oferta?: boolean;
+}): RegisterErrors {
+ const errors: RegisterErrors = {};
+
+ if (!data.name.trim()) {
+ errors.name = 'Name is required';
+ } else if (data.name.trim().length < 2) {
+ errors.name = 'Name must be at least 2 characters';
+ }
+
+ if (!data.surname.trim()) {
+ errors.surname = 'Surname is required';
+ } else if (data.surname.trim().length < 2) {
+ errors.surname = 'Surname must be at least 2 characters';
+ }
+
+ const digits = data.phone.replace(/\D/g, '');
+ if (!digits) {
+ errors.phone = 'Phone is required';
+ } else if (digits.length !== 9) {
+ errors.phone = 'Enter a valid 9-digit phone number';
+ }
+
+ if (!data.oferta) {
+ errors.oferta = 'You must accept the terms to continue';
+ }
+
+ return errors;
+}
diff --git a/src/features/auth/register/ui/form.tsx b/src/features/auth/register/ui/form.tsx
new file mode 100644
index 0000000..9db4a5f
--- /dev/null
+++ b/src/features/auth/register/ui/form.tsx
@@ -0,0 +1,249 @@
+'use client';
+
+import React, { useCallback } from 'react';
+import { useRegisterForm } from '../lib/useRegisterForm';
+import { formatPhone, normalizeDigits } from '@/shared/lib/formatPhone';
+import PhonePrefix from '@/shared/ui/phonePrefix';
+
+function Field({
+ id,
+ label,
+ type = 'text',
+ placeholder,
+ value,
+ name,
+ onChange,
+ error,
+}: {
+ id: string;
+ label: string;
+ type?: string;
+ placeholder?: string;
+ value: string;
+ name: string;
+ onChange: (e: React.ChangeEvent) => void;
+ error?: string;
+}) {
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+}
+
+export function RegisterFormUI() {
+ const {
+ registerData,
+ errors,
+ loading,
+ success,
+ handleChange,
+ handleOferta,
+ handleSubmit,
+ } = useRegisterForm();
+ const [phone, setPhone] = React.useState(registerData.phone || '');
+ const [isFocused, setIsFocused] = React.useState(false);
+ const handlePhoneChange = useCallback(
+ (e: React.ChangeEvent) => {
+ setPhone(normalizeDigits(e.target.value));
+ },
+ [setPhone],
+ );
+
+ // ── Success state ──────────────────────────────────────
+ if (success) {
+ return (
+
+
+
+ ✓
+
+
+ You‘re registered
+
+
+ We‘ll be in touch shortly.
+
+
+
+ );
+ }
+
+ // ── Form ───────────────────────────────────────────────
+ return (
+
+ {/* Header */}
+
+ Create account
+
+
+ Register
+
+
+ Fill in your details to get started.
+
+
+
+
+ );
+}
diff --git a/src/features/auth/register/ui/index.tsx b/src/features/auth/register/ui/index.tsx
new file mode 100644
index 0000000..1dc1b1d
--- /dev/null
+++ b/src/features/auth/register/ui/index.tsx
@@ -0,0 +1,39 @@
+import { MotionWrapper } from '@/shared/ui/motion';
+import { useRegisterModal } from '@/shared/zustand/auth';
+import { X } from 'lucide-react';
+import React from 'react';
+import { RegisterFormUI } from './form';
+
+export function RegisterForm() {
+ const toggleRegisterModal = useRegisterModal(
+ (state) => state.toggleRegisterModal,
+ );
+ return (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+
+
+ {/* Close button */}
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/features/providers/provider.tsx b/src/features/providers/provider.tsx
index 04c7ab9..863278c 100644
--- a/src/features/providers/provider.tsx
+++ b/src/features/providers/provider.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { AuthModals } from '../auth/login/model';
+import { AuthModals } from '../auth/model';
export default function Provider({ children }: { children: React.ReactNode }) {
return (
diff --git a/src/request/plagiarismapi.ts b/src/request/plagiarismapi.ts
new file mode 100644
index 0000000..b5ee827
--- /dev/null
+++ b/src/request/plagiarismapi.ts
@@ -0,0 +1,41 @@
+// ─── Constants ───────────────────────────────────────────────────────────────
+
+import {
+ PlagiarismSubmissionPayload,
+ PlagiarismSubmissionResponse,
+} from '@/widgets/fileUpload/lib/types';
+
+const API_BASE_URL = process.env.VITE_API_BASE_URL ?? '/api';
+const ENDPOINT = `${API_BASE_URL}/plagiarism/submit`;
+
+// ─── API Function ────────────────────────────────────────────────────────────
+
+/**
+ * Submits a document for plagiarism checking.
+ * Sends a multipart/form-data request to the backend API.
+ */
+export async function submitPlagiarismCheck(
+ payload: PlagiarismSubmissionPayload,
+): Promise {
+ const formData = new FormData();
+ formData.append('topic', payload.topic);
+ formData.append('senderFullName', payload.senderFullName);
+ formData.append('file', payload.file);
+ formData.append('withCertificate', String(payload.withCertificate));
+
+ const response = await fetch(ENDPOINT, {
+ method: 'POST',
+ body: formData,
+ // Do NOT set Content-Type manually — the browser sets it with the boundary
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.json().catch(() => ({}));
+ throw new Error(
+ (errorBody as { message?: string }).message ??
+ `Request failed with status ${response.status}`,
+ );
+ }
+
+ return response.json() as Promise;
+}
diff --git a/src/shared/lib/formatPhone.ts b/src/shared/lib/formatPhone.ts
index bab88bd..c925370 100644
--- a/src/shared/lib/formatPhone.ts
+++ b/src/shared/lib/formatPhone.ts
@@ -1,38 +1,14 @@
-/**
- * Format the number (+998 00 111-22-33)
- * @param value Number to be formatted
- * @returns string +998 00 111-22-33
- */
const formatPhone = (value: string) => {
- // Keep only numbers
- const digits = value.replace(/\D/g, '');
-
- // Return empty string if data is not available
- if (digits.length === 0) {
- return '';
- }
-
- const prefix = digits.startsWith('998') ? '+998 ' : '+998 ';
-
- let formattedNumber = prefix;
-
- if (digits.length > 3) {
- formattedNumber += digits.slice(3, 5);
- }
-
- if (digits.length > 5) {
- formattedNumber += ' ' + digits.slice(5, 8);
- }
-
- if (digits.length > 8) {
- formattedNumber += '-' + digits.slice(8, 10);
- }
-
- if (digits.length > 10) {
- formattedNumber += '-' + digits.slice(10, 12);
- }
-
- return formattedNumber.trim();
+ if (value.length <= 2) return value;
+ if (value.length <= 5) return `${value.slice(0, 2)} ${value.slice(2)}`;
+ if (value.length <= 7)
+ return `${value.slice(0, 2)} ${value.slice(2, 5)} ${value.slice(5)}`;
+ return `${value.slice(0, 2)} ${value.slice(2, 5)} ${value.slice(
+ 5,
+ 7,
+ )} ${value.slice(7)}`;
};
-export default formatPhone;
+const normalizeDigits = (value: string) => value.replace(/\D/g, '').slice(0, 9);
+
+export { formatPhone, normalizeDigits };
diff --git a/src/features/auth/login/ui/motion.tsx b/src/shared/ui/motion.tsx
similarity index 100%
rename from src/features/auth/login/ui/motion.tsx
rename to src/shared/ui/motion.tsx
diff --git a/src/features/auth/login/ui/phonePrefix.tsx b/src/shared/ui/phonePrefix.tsx
similarity index 100%
rename from src/features/auth/login/ui/phonePrefix.tsx
rename to src/shared/ui/phonePrefix.tsx
diff --git a/src/shared/zustand/auth.ts b/src/shared/zustand/auth.ts
new file mode 100644
index 0000000..69d5482
--- /dev/null
+++ b/src/shared/zustand/auth.ts
@@ -0,0 +1,26 @@
+import { create } from 'zustand';
+
+interface LoginModalStore {
+ openLoginModal: boolean;
+ toggleLoginModal: () => void;
+}
+
+const useLoginModal = create((set) => ({
+ openLoginModal: false,
+ toggleLoginModal: () =>
+ set((state) => ({ openLoginModal: !state.openLoginModal })),
+}));
+
+// REGISTER MODAL STORE ====================== ////////
+interface RegisterModalStore {
+ openRegisterModal: boolean;
+ toggleRegisterModal: () => void;
+}
+
+const useRegisterModal = create((set) => ({
+ openRegisterModal: false,
+ toggleRegisterModal: () =>
+ set((state) => ({ openRegisterModal: !state.openRegisterModal })),
+}));
+
+export { useLoginModal, useRegisterModal };
diff --git a/src/widgets/fileUpload/index.ts b/src/widgets/fileUpload/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/widgets/fileUpload/lib/types.ts b/src/widgets/fileUpload/lib/types.ts
new file mode 100644
index 0000000..a966a7c
--- /dev/null
+++ b/src/widgets/fileUpload/lib/types.ts
@@ -0,0 +1,44 @@
+// ─── Domain Types ───────────────────────────────────────────────────────────
+
+export interface User {
+ id: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+}
+
+export interface PlagiarismSubmissionPayload {
+ topic: string;
+ senderFullName: string;
+ file: File;
+ withCertificate: boolean;
+}
+
+export interface PlagiarismSubmissionResponse {
+ submissionId: string;
+ status: 'pending' | 'processing' | 'completed' | 'failed';
+ message: string;
+ certificateUrl?: string;
+}
+
+// ─── Form State Types ────────────────────────────────────────────────────────
+
+export interface PlagiarismFormState {
+ topic: string;
+ file: File | null;
+ withCertificate: boolean;
+}
+
+export type PlagiarismFormErrors = Partial<
+ Record
+>;
+
+// ─── UI State Types ──────────────────────────────────────────────────────────
+
+export type SubmissionStatus = 'idle' | 'loading' | 'success' | 'error';
+
+export interface SubmissionState {
+ status: SubmissionStatus;
+ response: PlagiarismSubmissionResponse | null;
+ error: string | null;
+}
diff --git a/src/widgets/fileUpload/lib/usePlagiraism.ts b/src/widgets/fileUpload/lib/usePlagiraism.ts
new file mode 100644
index 0000000..92adb8b
--- /dev/null
+++ b/src/widgets/fileUpload/lib/usePlagiraism.ts
@@ -0,0 +1,105 @@
+'use client';
+import { useState, useCallback } from 'react';
+import {
+ PlagiarismFormErrors,
+ PlagiarismFormState,
+ SubmissionState,
+} from './types';
+import { selectFullName, useUserStore } from './userStore';
+import { isFormValid, validatePlagiarismForm } from './validation';
+import { submitPlagiarismCheck } from '@/request/plagiarismapi';
+
+// ─── Initial States ──────────────────────────────────────────────────────────
+
+const INITIAL_FORM: PlagiarismFormState = {
+ topic: '',
+ file: null,
+ withCertificate: false,
+};
+
+const INITIAL_SUBMISSION: SubmissionState = {
+ status: 'idle',
+ response: null,
+ error: null,
+};
+
+// ─── Hook ────────────────────────────────────────────────────────────────────
+
+export function usePlagiarismForm() {
+ const senderFullName = useUserStore(selectFullName);
+
+ const [form, setForm] = useState(INITIAL_FORM);
+ const [errors, setErrors] = useState({});
+ const [submission, setSubmission] =
+ useState(INITIAL_SUBMISSION);
+
+ // ── Field updaters ───────────────────────────────────────────────────────
+
+ const setTopic = useCallback((topic: string) => {
+ setForm((prev) => ({ ...prev, topic }));
+ setErrors((prev) => ({ ...prev, topic: undefined }));
+ }, []);
+
+ const setFile = useCallback((file: File | null) => {
+ setForm((prev) => ({ ...prev, file }));
+ setErrors((prev) => ({ ...prev, file: undefined }));
+ }, []);
+
+ const toggleCertificate = useCallback(() => {
+ setForm((prev) => ({ ...prev, withCertificate: !prev.withCertificate }));
+ }, []);
+
+ // ── Submission ───────────────────────────────────────────────────────────
+
+ const handleSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const validationErrors = validatePlagiarismForm(form);
+ if (!isFormValid(validationErrors)) {
+ setErrors(validationErrors);
+ return;
+ }
+
+ 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); // Reset form 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(() => {
+ setSubmission(INITIAL_SUBMISSION);
+ }, []);
+
+ // ── Derived state ────────────────────────────────────────────────────────
+
+ const isLoading = submission.status === 'loading';
+
+ return {
+ form,
+ errors,
+ submission,
+ senderFullName,
+ isLoading,
+ setTopic,
+ setFile,
+ toggleCertificate,
+ handleSubmit,
+ resetSubmission,
+ };
+}
diff --git a/src/widgets/fileUpload/lib/userStore.ts b/src/widgets/fileUpload/lib/userStore.ts
new file mode 100644
index 0000000..6ed7b36
--- /dev/null
+++ b/src/widgets/fileUpload/lib/userStore.ts
@@ -0,0 +1,33 @@
+import { create } from 'zustand';
+import { User } from './types';
+
+// ─── Store Interface ─────────────────────────────────────────────────────────
+
+interface UserStore {
+ user: User | null;
+ isAuthenticated: boolean;
+ setUser: (user: User) => void;
+ clearUser: () => void;
+}
+
+// ─── Store Implementation ────────────────────────────────────────────────────
+
+export const useUserStore = create((set) => ({
+ // Mock: pre-populated as if the user is already logged in
+ user: {
+ id: 'usr_01',
+ firstName: 'Amir',
+ lastName: 'Karimov',
+ email: 'amir.karimov@example.com',
+ },
+ isAuthenticated: true,
+
+ setUser: (user) => set({ user, isAuthenticated: true }),
+ clearUser: () => set({ user: null, isAuthenticated: false }),
+}));
+
+// ─── Selectors ───────────────────────────────────────────────────────────────
+
+export const selectUser = (state: UserStore) => state.user;
+export const selectFullName = (state: UserStore): string =>
+ state.user ? `${state.user.firstName} ${state.user.lastName}` : '';
diff --git a/src/widgets/fileUpload/lib/validation.ts b/src/widgets/fileUpload/lib/validation.ts
new file mode 100644
index 0000000..ab6b4fd
--- /dev/null
+++ b/src/widgets/fileUpload/lib/validation.ts
@@ -0,0 +1,50 @@
+// ─── Constants ───────────────────────────────────────────────────────────────
+
+import { PlagiarismFormErrors, PlagiarismFormState } from './types';
+
+const MAX_FILE_SIZE_MB = 20;
+const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
+
+const ALLOWED_MIME_TYPES = [
+ 'application/pdf',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'text/plain',
+];
+
+const ALLOWED_EXTENSIONS = '.pdf, .doc, .docx, .txt';
+
+// ─── Validator ───────────────────────────────────────────────────────────────
+
+export function validatePlagiarismForm(
+ state: PlagiarismFormState,
+): PlagiarismFormErrors {
+ const errors: PlagiarismFormErrors = {};
+
+ // Topic validation
+ const trimmedTopic = state.topic.trim();
+ if (!trimmedTopic) {
+ errors.topic = 'Topic is required.';
+ } else if (trimmedTopic.length < 3) {
+ errors.topic = 'Topic must be at least 3 characters.';
+ } else if (trimmedTopic.length > 200) {
+ errors.topic = 'Topic must not exceed 200 characters.';
+ }
+
+ // File validation
+ if (!state.file) {
+ errors.file = 'Please upload a document.';
+ } else {
+ if (state.file.size > MAX_FILE_SIZE_BYTES) {
+ errors.file = `File size must not exceed ${MAX_FILE_SIZE_MB} MB.`;
+ } else if (!ALLOWED_MIME_TYPES.includes(state.file.type)) {
+ errors.file = `Unsupported file type. Allowed: ${ALLOWED_EXTENSIONS}.`;
+ }
+ }
+
+ return errors;
+}
+
+export function isFormValid(errors: PlagiarismFormErrors): boolean {
+ return Object.keys(errors).length === 0;
+}
diff --git a/src/widgets/fileUpload/ui/Plagiraismcheckform.tsx b/src/widgets/fileUpload/ui/Plagiraismcheckform.tsx
new file mode 100644
index 0000000..fe414ed
--- /dev/null
+++ b/src/widgets/fileUpload/ui/Plagiraismcheckform.tsx
@@ -0,0 +1,163 @@
+'use client';
+import React from 'react';
+import {
+ FieldWrapper,
+ TextInput,
+ ReadonlyField,
+ FileUploadField,
+ CertificateCheckbox,
+ SubmitButton,
+ StatusBanner,
+} from './Plagiraismui';
+import { usePlagiarismForm } from '../lib/usePlagiraism';
+
+// ─── UserIcon (inline) ───────────────────────────────────────────────────────
+
+function UserIcon() {
+ return (
+
+ );
+}
+
+// ─── Component ───────────────────────────────────────────────────────────────
+
+export function PlagiarismCheckForm() {
+ const {
+ form,
+ errors,
+ submission,
+ senderFullName,
+ isLoading,
+ setTopic,
+ setFile,
+ toggleCertificate,
+ handleSubmit,
+ resetSubmission,
+ } = usePlagiarismForm();
+
+ return (
+
+
+ {/* ── Header ────────────────────────────────────────────────────── */}
+
+
+
+ Originality Check
+
+
+ Submit Your Document
+
+
+ Upload a document to verify its originality. Results are typically
+ ready within a few minutes.
+
+
+
+ {/* ── Card ──────────────────────────────────────────────────────── */}
+
+ {/* Progress bar accent */}
+
+
+
+
+
+ {/* Footer note */}
+
+ Your document is processed securely and not stored beyond the analysis
+ period.
+
+
+
+ );
+}
diff --git a/src/widgets/fileUpload/ui/Plagiraismui.tsx b/src/widgets/fileUpload/ui/Plagiraismui.tsx
new file mode 100644
index 0000000..79f7da9
--- /dev/null
+++ b/src/widgets/fileUpload/ui/Plagiraismui.tsx
@@ -0,0 +1,431 @@
+import React from 'react';
+
+// ─── FieldWrapper ────────────────────────────────────────────────────────────
+
+interface FieldWrapperProps {
+ label: string;
+ htmlFor?: string;
+ error?: string;
+ required?: boolean;
+ children: React.ReactNode;
+}
+
+export function FieldWrapper({
+ label,
+ htmlFor,
+ error,
+ required,
+ children,
+}: FieldWrapperProps) {
+ return (
+
+
+ {children}
+ {error && (
+
+
+ {error}
+
+ )}
+
+ );
+}
+
+// ─── TextInput ───────────────────────────────────────────────────────────────
+
+interface TextInputProps extends React.InputHTMLAttributes {
+ hasError?: boolean;
+}
+
+export function TextInput({
+ hasError,
+ className = '',
+ ...props
+}: TextInputProps) {
+ return (
+
+ );
+}
+
+// ─── ReadonlyField ───────────────────────────────────────────────────────────
+
+interface ReadonlyFieldProps {
+ value: string;
+ icon?: React.ReactNode;
+}
+
+export function ReadonlyField({ value, icon }: ReadonlyFieldProps) {
+ return (
+
+ {icon && {icon}}
+ {value}
+
+ Auto-filled
+
+
+ );
+}
+
+// ─── FileUploadField ─────────────────────────────────────────────────────────
+
+interface FileUploadFieldProps {
+ file: File | null;
+ onFileChange: (file: File | null) => void;
+ hasError?: boolean;
+ accept?: string;
+}
+
+export function FileUploadField({
+ file,
+ onFileChange,
+ hasError,
+ accept = '.pdf,.doc,.docx,.txt',
+}: FileUploadFieldProps) {
+ const inputRef = React.useRef(null);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const selected = e.target.files?.[0] ?? null;
+ onFileChange(selected);
+ // Reset so the same file can be re-selected after removal
+ e.target.value = '';
+ };
+
+ const handleRemove = () => {
+ onFileChange(null);
+ };
+
+ const formatBytes = (bytes: number) => {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ };
+
+ return (
+
+
+
+ {!file ? (
+
+ ) : (
+
+
+
+
+
+
+ {file.name}
+
+
{formatBytes(file.size)}
+
+
+
+ )}
+
+ );
+}
+
+// ─── CertificateCheckbox ─────────────────────────────────────────────────────
+
+interface CertificateCheckboxProps {
+ checked: boolean;
+ onChange: () => void;
+}
+
+export function CertificateCheckbox({
+ checked,
+ onChange,
+}: CertificateCheckboxProps) {
+ return (
+
+ );
+}
+
+// ─── SubmitButton ────────────────────────────────────────────────────────────
+
+interface SubmitButtonProps {
+ isLoading: boolean;
+}
+
+export function SubmitButton({ isLoading }: SubmitButtonProps) {
+ return (
+
+ );
+}
+
+// ─── StatusBanner ────────────────────────────────────────────────────────────
+
+interface StatusBannerProps {
+ status: 'success' | 'error';
+ message: string;
+ onDismiss: () => void;
+}
+
+export function StatusBanner({
+ status,
+ message,
+ onDismiss,
+}: StatusBannerProps) {
+ const isSuccess = status === 'success';
+ return (
+
+
+ {isSuccess ? '✓' : '✕'}
+
+
+ {message}
+
+
+
+ );
+}
+
+// ─── Inline SVG Icons ────────────────────────────────────────────────────────
+
+function UploadIcon({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
+
+function DocumentIcon({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
+
+function XIcon() {
+ return (
+
+ );
+}
+
+function ShieldIcon() {
+ return (
+
+ );
+}
+
+function SpinnerIcon() {
+ return (
+
+ );
+}
diff --git a/src/widgets/footer/ui/index.tsx b/src/widgets/footer/ui/index.tsx
index 9b6a900..c32350b 100644
--- a/src/widgets/footer/ui/index.tsx
+++ b/src/widgets/footer/ui/index.tsx
@@ -1,75 +1,30 @@
import { PRODUCT_INFO } from '@/shared/constants/data';
-import { InstagramIcon, YoutubeIcon } from 'lucide-react';
-import { sections } from '../lib/data';
-import { ModeToggle } from '@/shared/ui/theme-toggle';
-
const Footer = () => {
+ const shortLinks = [
+ { name: 'About', href: '/about' },
+ { name: 'Contact', href: '/contact' },
+ ];
return (
-
+
-
-
- {/* Logo */}
-
+
- © {new Date().getFullYear()} {PRODUCT_INFO.creator}. All rights
+ © {new Date().getFullYear()} Felix IT Solutions. All rights
reserved.
diff --git a/src/widgets/navbar/ui/authButtons.tsx b/src/widgets/navbar/ui/authButtons.tsx
index 8822376..6f4b032 100644
--- a/src/widgets/navbar/ui/authButtons.tsx
+++ b/src/widgets/navbar/ui/authButtons.tsx
@@ -1,5 +1,4 @@
'use client';
-import { useLoginModal } from '@/features/auth/login/lib/togle';
import { Link } from '@/shared/config/i18n/navigation';
import { Button } from '@/shared/ui/button';
import { useUserLogin } from '@/shared/zustand/userLogin';
@@ -11,6 +10,7 @@ import {
} from '@/shared/ui/navigation-menu';
import SubMenuLink from './SubMenuLink';
import { ChangeLang } from './ChangeLang';
+import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
function AuthButtons() {
const auth = {
@@ -24,6 +24,9 @@ function AuthButtons() {
];
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
+ const toggleRegisterModal = useRegisterModal(
+ (state) => state.toggleRegisterModal,
+ );
const user = useUserLogin((state) => state.user);
if (user) {
@@ -49,7 +52,7 @@ function AuthButtons() {
-