diff --git a/package-lock.json b/package-lock.json
index 1ff4093..49ef2d3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,9 +27,9 @@
"@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.2.4",
"@tabler/icons-react": "^3.31.0",
- "@tanstack/react-query": "^5.76.0",
+ "@tanstack/react-query": "^5.96.0",
"@tanstack/react-table": "^8.21.3",
- "axios": "^1.12.2",
+ "axios": "^1.14.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
@@ -40,6 +40,7 @@
"next-themes": "^0.4.6",
"react": "^19.1.2",
"react-dom": "^19.1.2",
+ "react-toastify": "^11.0.5",
"recharts": "^2.15.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
@@ -3208,9 +3209,9 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.90.16",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz",
- "integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==",
+ "version": "5.96.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.0.tgz",
+ "integrity": "sha512-sfO3uQeol1BU7cRP6NYY7nAiX3GiNY20lI/dtSbKLwcIkYw/X+w/tEsQAkc544AfIhBX/IvH/QYtPHrPhyAKGw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3218,12 +3219,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.90.16",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz",
- "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==",
+ "version": "5.96.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.0.tgz",
+ "integrity": "sha512-6qbjdm1K5kizVKv9TNqhIN3doq2anRhdF2XaFMFSn4m8L22S69RV+FilvlyVT4RoJyMxtPU5rs4RpdFa/PEC7A==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.90.16"
+ "@tanstack/query-core": "5.96.0"
},
"funding": {
"type": "github",
@@ -4240,14 +4241,14 @@
}
},
"node_modules/axios": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
- "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
+ "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
- "proxy-from-env": "^1.1.0"
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^2.1.0"
}
},
"node_modules/axobject-query": {
@@ -7829,10 +7830,13 @@
}
},
"node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
},
"node_modules/punycode": {
"version": "2.3.1",
@@ -7976,6 +7980,19 @@
}
}
},
+ "node_modules/react-toastify": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
+ "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19",
+ "react-dom": "^18 || ^19"
+ }
+ },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
diff --git a/package.json b/package.json
index ea0c581..cd5bc6d 100644
--- a/package.json
+++ b/package.json
@@ -30,9 +30,9 @@
"@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.2.4",
"@tabler/icons-react": "^3.31.0",
- "@tanstack/react-query": "^5.76.0",
+ "@tanstack/react-query": "^5.96.0",
"@tanstack/react-table": "^8.21.3",
- "axios": "^1.12.2",
+ "axios": "^1.14.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
@@ -43,6 +43,7 @@
"next-themes": "^0.4.6",
"react": "^19.1.2",
"react-dom": "^19.1.2",
+ "react-toastify": "^11.0.5",
"recharts": "^2.15.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
index 229166b..298e695 100644
--- a/src/app/[locale]/layout.tsx
+++ b/src/app/[locale]/layout.tsx
@@ -11,6 +11,7 @@ import { setRequestLocale } from 'next-intl/server';
import QueryProvider from '@/shared/config/react-query/QueryProvider';
import Script from 'next/script';
import Provider from '@/features/providers/provider';
+import { ToastContainer } from 'react-toastify';
type Props = {
children: ReactNode;
@@ -45,6 +46,7 @@ export default async function RootLayout({ children, params }: Props) {
{children}
+
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
index 09d434c..330b1c1 100644
--- a/src/app/[locale]/page.tsx
+++ b/src/app/[locale]/page.tsx
@@ -1,11 +1,9 @@
-import { PlagiarismCheckForm } from '@/widgets/fileUpload/ui/Plagiraismcheckform';
-import { HistoryPage } from '@/widgets/history';
+import PlagiarismLanding from '@/widgets/home';
export default function Home() {
return (
);
}
diff --git a/src/app/[locale]/plagat/page.tsx b/src/app/[locale]/plagat/page.tsx
new file mode 100644
index 0000000..130c2f5
--- /dev/null
+++ b/src/app/[locale]/plagat/page.tsx
@@ -0,0 +1,11 @@
+import { PlagiarismCheckForm } from '@/widgets/fileUpload/ui/Plagiraismcheckform';
+import { HistoryPage } from '@/widgets/history';
+
+export default function Page() {
+ return (
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 70fb346..f7a5f68 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,6 +1,10 @@
+@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;0,700;1,400&family=DM+Mono:wght@400;500&family=DM+Sans:wght@400;500&display=swap');
@import 'tailwindcss';
@import 'tw-animate-css';
+*, *::before, *::after { box-sizing: border-box;}
+html { scroll-behavior: smooth; }
+
@custom-variant dark (&:is(.dark *));
@theme inline {
diff --git a/src/features/auth/login/lib/useLoginForm.ts b/src/features/auth/login/lib/useLoginForm.ts
index f7f26b1..5e1b5c6 100644
--- a/src/features/auth/login/lib/useLoginForm.ts
+++ b/src/features/auth/login/lib/useLoginForm.ts
@@ -2,10 +2,35 @@
import React, { useState } from 'react';
import { useTranslations } from 'next-intl';
+import { useMutation } from '@tanstack/react-query';
+import { apiRequest } from '@/shared/request/apiRequest';
+import { links } from '@/shared/request/links';
+import { useLoginModal } from '@/shared/zustand/auth';
+import { toast } from 'react-toastify';
+
+interface LoginData {
+ phone: string;
+}
export function useLoginForm() {
const [phone, setPhone] = useState('');
const [error, setError] = useState('');
+ const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
+ const loginReqest = useMutation({
+ mutationKey: ['login'],
+ mutationFn: (data: LoginData) => apiRequest('POST', links.login, data),
+ onSuccess: (data) => {
+ console.log('Login successful:', data);
+ toggleLoginModal();
+ toast.success('Kirish muvaffaqiyatli!');
+ },
+ onError: (err) => {
+ setError(err instanceof Error ? err.message : 'Unknown error');
+ // toggleLoginModal();
+ console.error('Login failed:', err);
+ toast.error(err instanceof Error ? err.message : 'Unknown error');
+ },
+ });
const t = useTranslations();
@@ -18,6 +43,7 @@ export function useLoginForm() {
return;
}
+ loginReqest.mutate({ phone: `+998${phone}` });
sessionStorage.setItem('prev_page', 'login');
};
diff --git a/src/features/auth/register/lib/useRegisterForm.ts b/src/features/auth/register/lib/useRegisterForm.ts
index 5121d52..07026fb 100644
--- a/src/features/auth/register/lib/useRegisterForm.ts
+++ b/src/features/auth/register/lib/useRegisterForm.ts
@@ -3,13 +3,43 @@
import { useCallback, useState } from 'react';
import { useRegisterZustand } from './registerZustand';
import { validateRegister, RegisterErrors } from './validateRegister';
+import { useRegisterModal } from '@/shared/zustand/auth';
+import { useMutation } from '@tanstack/react-query';
+import { apiRequest } from '@/shared/request/apiRequest';
+import { links } from '@/shared/request/links';
+import { toast } from 'react-toastify';
+
+interface RegisterData {
+ name: string;
+ surname: string;
+ phone: string;
+}
export function useRegisterForm() {
const { registerData, setItem, setOferta, clearRegisterData } =
useRegisterZustand();
const [errors, setErrors] = useState({});
- const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
+ const toggleRegisterModal = useRegisterModal(
+ (state) => state.toggleRegisterModal,
+ );
+
+ const registerRequest = useMutation({
+ mutationKey: ['register'],
+ mutationFn: (data: RegisterData) =>
+ apiRequest('POST', links.register, data),
+ onSuccess: (data) => {
+ console.log('Register successful:', data);
+ toggleRegisterModal();
+ setSuccess(true);
+ toast.success("Ro'yxatdan o'tish muvaffaqiyatli!");
+ },
+ onError: (err) => {
+ // toggleLoginModal();
+ console.error('Register failed:', err);
+ toast.error(err instanceof Error ? err.message : 'Unknown error');
+ },
+ });
const handleChange = useCallback(
(e: React.ChangeEvent) => {
@@ -33,23 +63,12 @@ export function useRegisterForm() {
async (e: React.FormEvent) => {
e.preventDefault();
const validationErrors = validateRegister(registerData);
+ console.log('register data on submit:', 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);
- }
+ registerRequest.mutate(registerData);
},
[registerData, clearRegisterData],
);
@@ -57,10 +76,11 @@ export function useRegisterForm() {
return {
registerData,
errors,
- loading,
+ loading: registerRequest.isPending,
success,
handleChange,
handleOferta,
handleSubmit,
+ setItem,
};
}
diff --git a/src/features/auth/register/lib/validateRegister.ts b/src/features/auth/register/lib/validateRegister.ts
index 898b29a..8063b42 100644
--- a/src/features/auth/register/lib/validateRegister.ts
+++ b/src/features/auth/register/lib/validateRegister.ts
@@ -28,8 +28,8 @@ export function validateRegister(data: {
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';
+ } else if (digits.length !== 9 && digits.length !== 12) {
+ errors.phone = 'Enter a valid 9-digit or 13-digit phone number';
}
if (!data.oferta) {
diff --git a/src/features/auth/register/ui/form.tsx b/src/features/auth/register/ui/form.tsx
index 9db4a5f..0269f5f 100644
--- a/src/features/auth/register/ui/form.tsx
+++ b/src/features/auth/register/ui/form.tsx
@@ -63,6 +63,7 @@ function Field({
}
export function RegisterFormUI() {
+ const [phone, setPhone] = React.useState('');
const {
registerData,
errors,
@@ -71,16 +72,22 @@ export function RegisterFormUI() {
handleChange,
handleOferta,
handleSubmit,
+ setItem,
} = 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));
+ if (normalizeDigits(e.target.value).length === 9)
+ setItem('phone', `998${phone}`);
},
[setPhone],
);
+ if (errors) {
+ console.log('Validation errors:', errors);
+ }
+
// ── Success state ──────────────────────────────────────
if (success) {
return (
diff --git a/src/shared/request/apiRequest.ts b/src/shared/request/apiRequest.ts
new file mode 100644
index 0000000..6e84250
--- /dev/null
+++ b/src/shared/request/apiRequest.ts
@@ -0,0 +1,29 @@
+import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
+
+import { getRouteLang } from './getLanguage';
+
+const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
+
+const api = axios.create({
+ baseURL: baseUrl,
+});
+
+export const apiRequest = async (
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
+ url: string,
+ data?: unknown,
+ config?: Omit,
+): Promise => {
+ const response: AxiosResponse = await api.request({
+ method,
+ url,
+ data,
+ ...config,
+ headers: {
+ 'Accept-Language': getRouteLang(), // evaluated per-request
+ ...config?.headers,
+ },
+ });
+
+ return response.data;
+};
diff --git a/src/shared/request/getLanguage.ts b/src/shared/request/getLanguage.ts
new file mode 100644
index 0000000..3e47b53
--- /dev/null
+++ b/src/shared/request/getLanguage.ts
@@ -0,0 +1,30 @@
+const defaultLocale = 'uz';
+const locales = ['uz', 'ru', 'en'] as const;
+
+export function getRouteLang(): string {
+ if (typeof window === 'undefined') return defaultLocale;
+
+ // 1)Fall back to the first non-empty pathname segment
+ const rawSegments = window.location.pathname.split('/'); // e.g. ['', 'uz', 'path', ...]
+ const segments = rawSegments.filter(Boolean); // removes empty strings
+ if (segments.length > 0) {
+ const candidate = segments[0]; // first segment after root
+ if (locales.includes(candidate as (typeof locales)[number]))
+ return candidate;
+ }
+
+ // 2) Try cookie NEXT_LOCALE first (language switcher sets it)
+ const cookieMatch = document.cookie
+ .split(';')
+ .map((c) => c.trim())
+ .find((c) => c.startsWith('NEXT_LOCALE='));
+
+ if (cookieMatch) {
+ const cookieLang = cookieMatch.split('=')[1];
+ if (locales.includes(cookieLang as (typeof locales)[number]))
+ return cookieLang;
+ }
+
+ // 3) final fallback
+ return defaultLocale;
+}
diff --git a/src/shared/request/links.ts b/src/shared/request/links.ts
new file mode 100644
index 0000000..a1859f4
--- /dev/null
+++ b/src/shared/request/links.ts
@@ -0,0 +1,4 @@
+export const links = {
+ login: '/users/login/',
+ register: '/users/register/',
+};
diff --git a/src/request/plagiarismapi.ts b/src/shared/request/plagiarismapi.ts
similarity index 100%
rename from src/request/plagiarismapi.ts
rename to src/shared/request/plagiarismapi.ts
diff --git a/src/widgets/detail/index.tsx b/src/widgets/detail/index.tsx
index 690c30b..d830519 100644
--- a/src/widgets/detail/index.tsx
+++ b/src/widgets/detail/index.tsx
@@ -15,7 +15,7 @@ interface InfoRowProps {
export const InfoRow: React.FC = ({ label, value, icon }) => (
-
+
{icon && {icon}}
{label}
@@ -150,7 +150,7 @@ export const SimilarityMeter: React.FC = ({
{/* Track */}
{/* Threshold markers */}
@@ -211,7 +211,7 @@ export const Avatar: React.FC
= ({
return (
{initials}
diff --git a/src/widgets/fileUpload/lib/usePlagiraism.ts b/src/widgets/fileUpload/lib/usePlagiraism.ts
index 6956277..f488db8 100644
--- a/src/widgets/fileUpload/lib/usePlagiraism.ts
+++ b/src/widgets/fileUpload/lib/usePlagiraism.ts
@@ -7,7 +7,7 @@ import {
} from './types';
import { selectFullName, useUserStore } from './userStore';
import { isFormValid, validatePlagiarismForm } from './validation';
-import { submitPlagiarismCheck } from '@/request/plagiarismapi';
+import { submitPlagiarismCheck } from '@/shared/request/plagiarismapi';
// ─── Initial States ──────────────────────────────────────────────────────────
diff --git a/src/widgets/home/animations.ts b/src/widgets/home/animations.ts
new file mode 100644
index 0000000..9470812
--- /dev/null
+++ b/src/widgets/home/animations.ts
@@ -0,0 +1,15 @@
+import type { Variants } from 'framer-motion';
+
+export const fadeUp = (delay = 0): Variants => ({
+ hidden: { opacity: 0, y: 28 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.65, delay, ease: [0.22, 1, 0.36, 1] },
+ },
+});
+
+export const stagger: Variants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.1 } },
+};
diff --git a/src/widgets/home/components/Badge.tsx b/src/widgets/home/components/Badge.tsx
new file mode 100644
index 0000000..9985a40
--- /dev/null
+++ b/src/widgets/home/components/Badge.tsx
@@ -0,0 +1,43 @@
+import type { FC, ReactNode } from 'react';
+import { motion } from 'framer-motion';
+import { C } from '../tokens';
+
+interface BadgeProps {
+ children: ReactNode;
+}
+
+const Badge: FC = ({ children }) => (
+
+
+ {children}
+
+);
+
+export default Badge;
diff --git a/src/widgets/home/components/DocIllustration.tsx b/src/widgets/home/components/DocIllustration.tsx
new file mode 100644
index 0000000..a72358e
--- /dev/null
+++ b/src/widgets/home/components/DocIllustration.tsx
@@ -0,0 +1,137 @@
+import type { FC } from 'react';
+import { motion } from 'framer-motion';
+import { C } from '../tokens';
+import { DOC_LINE_WIDTHS } from '../constants';
+
+const DocIllustration: FC = () => (
+
+ {/* Shadow card behind */}
+
+
+ {/* Main document card */}
+
+ {/* Card header */}
+
+
+ {/* Skeleton lines */}
+ {DOC_LINE_WIDTHS.map((w, i) => (
+
+ ))}
+
+ {/* Similarity warning */}
+
+ ⚠ 12% similarity found
+
+
+ {/* Verified badge */}
+
+ ✓ VERIFIED
+
+
+
+);
+
+export default DocIllustration;
diff --git a/src/widgets/home/components/Hero.tsx b/src/widgets/home/components/Hero.tsx
new file mode 100644
index 0000000..3b88a6a
--- /dev/null
+++ b/src/widgets/home/components/Hero.tsx
@@ -0,0 +1,177 @@
+import type { FC } from 'react';
+import { motion, type MotionStyle, type MotionValue } from 'framer-motion';
+import { fadeUp, stagger } from '../animations';
+import { C } from '../tokens';
+import { STATS } from '../constants';
+import { useIsMobile } from '../hooks/useIsMobile';
+import Badge from './Badge';
+import DocIllustration from './DocIllustration';
+import Section from './Section';
+import Stat from './Stat';
+import StartButton from './StartButton';
+
+interface HeroProps {
+ onStart: () => void;
+ blobY: MotionValue;
+}
+
+const Hero: FC = ({ onStart, blobY }) => {
+ const isMobile = useIsMobile();
+
+ return (
+
+ {/* Dot grid background */}
+
+
+ {/* Ambient glow blob */}
+
+
+
+ {/* Main row */}
+
+ {/* Text block */}
+
+ Academic Integrity Platform
+
+
+
+ Is Your Work
+
+
+ Truly Original?
+
+
+
+
+ Plagiarism is presenting someone else‘s ideas or words as
+ your own. In academia and professional life, it carries serious
+ consequences. Our platform detects it in seconds — so you can
+ submit with full confidence.
+
+
+
+
+
+ Certificate issued within 24h
+
+
+
+
+ {/* Illustration (hidden on mobile) */}
+ {!isMobile &&
}
+
+
+ {/* Stats row */}
+
+ {STATS.map((s) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default Hero;
diff --git a/src/widgets/home/components/InfoCard.tsx b/src/widgets/home/components/InfoCard.tsx
new file mode 100644
index 0000000..9454283
--- /dev/null
+++ b/src/widgets/home/components/InfoCard.tsx
@@ -0,0 +1,87 @@
+import { useRef, type FC } from 'react';
+import { motion, useInView } from 'framer-motion';
+import { fadeUp } from '../animations';
+import { C } from '../tokens';
+import type { InfoCardData } from '../types';
+
+const InfoCard: FC = ({
+ title,
+ text,
+ icon,
+ accent,
+ bg,
+ delay,
+}) => {
+ const ref = useRef(null);
+ const inView = useInView(ref, { once: true, margin: '-40px' });
+
+ return (
+
+ {/* Top accent bar */}
+
+
+ {/* Icon */}
+
+ {icon}
+
+
+
+ {title}
+
+
+
+ {text}
+
+
+ );
+};
+
+export default InfoCard;
diff --git a/src/widgets/home/components/InfoSection.tsx b/src/widgets/home/components/InfoSection.tsx
new file mode 100644
index 0000000..35ca690
--- /dev/null
+++ b/src/widgets/home/components/InfoSection.tsx
@@ -0,0 +1,55 @@
+import type { FC } from 'react';
+import { motion } from 'framer-motion';
+import { fadeUp } from '../animations';
+import { C } from '../tokens';
+import { INFO_CARDS } from '../constants';
+import InfoCard from './InfoCard';
+import Section from './Section';
+
+const InfoSection: FC = () => (
+
+
+ {/* Heading */}
+
+
+ Why It Matters
+
+
+ Understanding Plagiarism
+
+
+
+ {/* Cards */}
+
+ {INFO_CARDS.map((card) => (
+
+ ))}
+
+
+
+);
+
+export default InfoSection;
diff --git a/src/widgets/home/components/Section.tsx b/src/widgets/home/components/Section.tsx
new file mode 100644
index 0000000..a6bb7ed
--- /dev/null
+++ b/src/widgets/home/components/Section.tsx
@@ -0,0 +1,16 @@
+import type { CSSProperties, FC, ReactNode } from 'react';
+
+interface SectionProps {
+ children: ReactNode;
+ style?: CSSProperties;
+}
+
+const Section: FC = ({ children, style = {} }) => (
+
+);
+
+export default Section;
diff --git a/src/widgets/home/components/StartButton.tsx b/src/widgets/home/components/StartButton.tsx
new file mode 100644
index 0000000..4d2bde4
--- /dev/null
+++ b/src/widgets/home/components/StartButton.tsx
@@ -0,0 +1,43 @@
+'use client';
+import { useState, type FC } from 'react';
+import { motion } from 'framer-motion';
+import { C } from '../tokens';
+
+interface StartButtonProps {
+ onClick: () => void;
+ small?: boolean;
+}
+
+const StartButton: FC = ({ onClick, small = false }) => {
+ const [hovered, setHovered] = useState(false);
+
+ return (
+ setHovered(true)}
+ onHoverEnd={() => setHovered(false)}
+ onClick={onClick}
+ style={{
+ padding: small ? '10px 28px' : '15px 44px',
+ borderRadius: 6,
+ border: 'none',
+ background: hovered ? C.accentHover : C.accent,
+ color: '#fff',
+ fontFamily: "'DM Mono', monospace",
+ fontSize: small ? 11 : 13,
+ fontWeight: 500,
+ letterSpacing: '0.07em',
+ textTransform: 'uppercase',
+ cursor: 'pointer',
+ transition: 'background 0.2s, box-shadow 0.25s',
+ boxShadow: hovered
+ ? `0 8px 28px ${C.accent}44, 0 2px 8px ${C.accent}22`
+ : `0 2px 8px ${C.accent}22`,
+ }}
+ >
+ Start Checking →
+
+ );
+};
+
+export default StartButton;
diff --git a/src/widgets/home/components/Stat.tsx b/src/widgets/home/components/Stat.tsx
new file mode 100644
index 0000000..9adfe52
--- /dev/null
+++ b/src/widgets/home/components/Stat.tsx
@@ -0,0 +1,32 @@
+import type { FC } from 'react';
+import { C } from '../tokens';
+import type { StatItem } from '../types';
+
+const Stat: FC = ({ value, label }) => (
+
+
+ {value}
+
+
+ {label}
+
+
+);
+
+export default Stat;
diff --git a/src/widgets/home/components/StepCard.tsx b/src/widgets/home/components/StepCard.tsx
new file mode 100644
index 0000000..2fc7b39
--- /dev/null
+++ b/src/widgets/home/components/StepCard.tsx
@@ -0,0 +1,108 @@
+import { useRef, type FC } from 'react';
+import { motion, useInView } from 'framer-motion';
+import { fadeUp } from '../animations';
+import { C } from '../tokens';
+import { STEPS } from '../constants';
+import type { Step } from '../types';
+
+interface StepCardProps {
+ step: Step;
+ index: number;
+}
+
+const StepCard: FC = ({ step, index }) => {
+ const ref = useRef(null);
+ const inView = useInView(ref, { once: true, margin: '-50px' });
+ const isLast = index === STEPS.length - 1;
+
+ return (
+
+ {/* Vertical connector line */}
+ {!isLast && (
+
+ )}
+
+ {/* Step number badge */}
+
+ {step.num}
+
+
+ {/* Step content */}
+
+
+ {step.icon}
+
+ {step.title}
+
+
+
+ {step.desc}
+
+
+
+ );
+};
+
+export default StepCard;
diff --git a/src/widgets/home/components/StepsSection.tsx b/src/widgets/home/components/StepsSection.tsx
new file mode 100644
index 0000000..f203ff7
--- /dev/null
+++ b/src/widgets/home/components/StepsSection.tsx
@@ -0,0 +1,129 @@
+import type { FC } from 'react';
+import { motion } from 'framer-motion';
+import { fadeUp } from '../animations';
+import { C } from '../tokens';
+import { STEPS } from '../constants';
+import { useIsMobile } from '../hooks/useIsMobile';
+import Section from './Section';
+import StartButton from './StartButton';
+import StepCard from './StepCard';
+
+interface StepsSectionProps {
+ stepsRef: React.RefObject;
+ onScrollTop: () => void;
+}
+
+const StepsSection: FC = ({ stepsRef, onScrollTop }) => {
+ const isMobile = useIsMobile();
+
+ return (
+
+
+
+ {/* Heading */}
+
+
+ Process
+
+
+ How It Works
+
+
+ Six simple steps from upload to certified report.
+
+
+
+ {/* Steps grid */}
+
+ {STEPS.map((step, i) => (
+
+ ))}
+
+
+ {/* CTA banner */}
+
+
+
+ Ready to verify your document?
+
+
+ Get your originality certificate in under 24 hours.
+
+
+
+
+
+
+
+ );
+};
+
+export default StepsSection;
diff --git a/src/widgets/home/components/Ticker.tsx b/src/widgets/home/components/Ticker.tsx
new file mode 100644
index 0000000..688396b
--- /dev/null
+++ b/src/widgets/home/components/Ticker.tsx
@@ -0,0 +1,51 @@
+import type { FC } from 'react';
+import { motion } from 'framer-motion';
+import { C } from '../tokens';
+import { TICKER_ITEMS } from '../constants';
+
+const Ticker: FC = () => {
+ const doubled = [...TICKER_ITEMS, ...TICKER_ITEMS];
+
+ return (
+
+
+ {doubled.map((item, i) => (
+
+ {item}
+
+ ✦
+
+
+ ))}
+
+
+ );
+};
+
+export default Ticker;
diff --git a/src/widgets/home/constants.ts b/src/widgets/home/constants.ts
new file mode 100644
index 0000000..764ff44
--- /dev/null
+++ b/src/widgets/home/constants.ts
@@ -0,0 +1,86 @@
+import { C } from './tokens';
+import type { InfoCardData, StatItem, Step } from './types';
+
+export const STEPS: Step[] = [
+ {
+ num: '01',
+ icon: '▶',
+ title: 'Click Start',
+ desc: 'Hit the Start button on the homepage to kick off the process.',
+ },
+ {
+ num: '02',
+ icon: '🔐',
+ title: 'Register or Log In',
+ desc: 'Create a free account or sign in to access the checking platform.',
+ },
+ {
+ num: '03',
+ icon: '📄',
+ title: 'Upload Document',
+ desc: "Upload your file and enter the document's topic or theme.",
+ },
+ {
+ num: '04',
+ icon: '📤',
+ title: 'Send for Check',
+ desc: 'Submit your document for deep plagiarism analysis.',
+ },
+ {
+ num: '05',
+ icon: '💳',
+ title: 'Complete Payment',
+ desc: 'Pay securely for the plagiarism checking service.',
+ },
+ {
+ num: '06',
+ icon: '📜',
+ title: 'Get Your Report',
+ desc: 'Receive your official certificate and full plagiarism report.',
+ },
+];
+
+export const STATS: StatItem[] = [
+ { value: '98.7%', label: 'Detection accuracy' },
+ { value: '50K+', label: 'Documents checked' },
+ { value: '12+', label: 'Supported formats' },
+ { value: '24h', label: 'Report turnaround' },
+];
+
+export const INFO_CARDS: InfoCardData[] = [
+ {
+ delay: 0,
+ accent: C.accent,
+ bg: C.accentLight,
+ icon: '📖',
+ title: 'What is Plagiarism?',
+ text: "Plagiarism is presenting another person's ideas, words, or creative work without proper attribution. It ranges from direct copy-paste to subtle paraphrasing — both are equally problematic in academic or professional writing.",
+ },
+ {
+ delay: 0.1,
+ accent: C.red,
+ bg: C.redLight,
+ icon: '🛡️',
+ title: 'Why Check Your Document?',
+ text: 'Unintentional plagiarism is more common than you think. Checking before submission protects your reputation, ensures proper citation, meets institutional requirements, and gives you peace of mind.',
+ },
+ {
+ delay: 0.2,
+ accent: C.green,
+ bg: C.greenLight,
+ icon: '📜',
+ title: 'What You Get',
+ text: 'A comprehensive originality report with matched sources, similarity percentages, and an official signed certificate — accepted by universities, publishers, and institutions worldwide.',
+ },
+];
+
+export const TICKER_ITEMS: string[] = [
+ 'Originality Verified',
+ 'Academic Integrity',
+ 'Trusted Reports',
+ 'Deep Analysis',
+ 'Fast Results',
+ 'Official Certificate',
+];
+
+export const DOC_LINE_WIDTHS: number[] = [100, 88, 95, 72, 90, 60];
diff --git a/src/widgets/home/hooks/useIsMobile.ts b/src/widgets/home/hooks/useIsMobile.ts
new file mode 100644
index 0000000..43597f6
--- /dev/null
+++ b/src/widgets/home/hooks/useIsMobile.ts
@@ -0,0 +1,16 @@
+'use client';
+import { useEffect, useState } from 'react';
+
+export function useIsMobile(breakpoint = 768): boolean {
+ const [isMobile, setIsMobile] = useState(
+ typeof window !== 'undefined' ? window.innerWidth <= breakpoint : false,
+ );
+
+ useEffect(() => {
+ const handler = () => setIsMobile(window.innerWidth <= breakpoint);
+ window.addEventListener('resize', handler);
+ return () => window.removeEventListener('resize', handler);
+ }, [breakpoint]);
+
+ return isMobile;
+}
diff --git a/src/widgets/home/index.tsx b/src/widgets/home/index.tsx
new file mode 100644
index 0000000..51c310a
--- /dev/null
+++ b/src/widgets/home/index.tsx
@@ -0,0 +1,29 @@
+'use client';
+import { useRef, type FC } from 'react';
+import { useScroll, useTransform } from 'framer-motion';
+import Hero from './components/Hero';
+import InfoSection from './components/InfoSection';
+import StepsSection from './components/StepsSection';
+import Ticker from './components/Ticker';
+
+const PlagiarismLanding: FC = () => {
+ const stepsRef = useRef(null);
+ const { scrollY } = useScroll();
+ const blobY = useTransform(scrollY, [0, 600], [0, 80]);
+
+ const scrollToSteps = () =>
+ stepsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+
+ const scrollToTop = () => window.scrollTo({ top: 0, behavior: 'smooth' });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default PlagiarismLanding;
diff --git a/src/widgets/home/tokens.ts b/src/widgets/home/tokens.ts
new file mode 100644
index 0000000..c824688
--- /dev/null
+++ b/src/widgets/home/tokens.ts
@@ -0,0 +1,20 @@
+export const C = {
+ bg: '#F7F5F0',
+ surface: '#FFFFFF',
+ surfaceWarm: '#F0EDE6',
+ border: '#E2DDD6',
+ borderDark: '#C8C2B8',
+ accent: '#1A4FD6',
+ accentLight: '#EEF2FD',
+ accentHover: '#1240B8',
+ text: '#1A1814',
+ textMid: '#4A4640',
+ textMuted: '#8A8680',
+ green: '#1A7A4A',
+ greenLight: '#E8F5EE',
+ red: '#C0392B',
+ redLight: '#FDF0EE',
+ gold: '#B8860B',
+ shadow: 'rgba(26,22,16,0.07)',
+ shadowMd: 'rgba(26,22,16,0.13)',
+} as const;
diff --git a/src/widgets/home/types.ts b/src/widgets/home/types.ts
new file mode 100644
index 0000000..36369fc
--- /dev/null
+++ b/src/widgets/home/types.ts
@@ -0,0 +1,20 @@
+export interface Step {
+ num: string;
+ icon: string;
+ title: string;
+ desc: string;
+}
+
+export interface StatItem {
+ value: string;
+ label: string;
+}
+
+export interface InfoCardData {
+ delay: number;
+ accent: string;
+ bg: string;
+ icon: string;
+ title: string;
+ text: string;
+}
diff --git a/src/widgets/navbar/ui/index.tsx b/src/widgets/navbar/ui/index.tsx
index 6f1abb8..ec0da6f 100644
--- a/src/widgets/navbar/ui/index.tsx
+++ b/src/widgets/navbar/ui/index.tsx
@@ -20,8 +20,8 @@ const Navbar = () => {
const menu = getMenu(t);
return (
-