This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-01 19:20:00 +05:00
parent 7b76901f5f
commit dfa3e14d58
8 changed files with 232 additions and 31 deletions

View File

@@ -8,7 +8,7 @@ import { links } from '@/shared/request/links';
import { useLoginModal } from '@/shared/zustand/auth';
import { toast } from 'react-toastify';
import { useRouter } from '@/shared/config/i18n/navigation';
import { useUserStore } from '@/shared/zustand/user';
import { useUserPlagiatStore } from '@/shared/zustand/user';
interface LoginData {
phone: string;
@@ -28,7 +28,7 @@ export function useLoginForm() {
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const setUser = useUserStore((state) => state.setUser);
const setUser = useUserPlagiatStore((state) => state.setUser);
const route = useRouter();
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
const loginReqest = useMutation({

View File

@@ -10,12 +10,13 @@ import { links } from '@/shared/request/links';
import { toast } from 'react-toastify';
import { useRouter } from '@/shared/config/i18n/navigation';
import { AuthData } from '../../login/lib/useLoginForm';
import { useUserStore } from '@/shared/zustand/user';
import { useUserPlagiatStore } from '@/shared/zustand/user';
interface RegisterData {
name: string;
surname: string;
first_name: string;
last_name: string;
phone: string;
password: string;
}
export function useRegisterForm() {
@@ -23,7 +24,7 @@ export function useRegisterForm() {
useRegisterZustand();
const [errors, setErrors] = useState<RegisterErrors>({});
const [success, setSuccess] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setUser = useUserPlagiatStore((state) => state.setUser);
const route = useRouter();
const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal,
@@ -88,7 +89,13 @@ export function useRegisterForm() {
setErrors(validationErrors);
return;
}
registerRequest.mutate(registerData);
const sendedData = {
phone: registerData.phone,
first_name: registerData.name,
last_name: registerData.surname,
password: registerData.password,
};
registerRequest.mutate(sendedData);
},
[registerData, clearRegisterData],
);

View File

@@ -1,31 +1,198 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import axios, {
AxiosRequestConfig,
AxiosResponse,
AxiosError,
InternalAxiosRequestConfig,
} from 'axios';
import { getRouteLang } from './getLanguage';
// ─── Constants ─────────────────────────────────────────────────────────────────
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
const DEFAULT_LOCALE = 'uz'; // fallback locale for redirect
// ─── Token helpers ─────────────────────────────────────────────────────────────
// Adjust key names to match whatever your auth stores them under.
export const TokenStorage = {
getAccess: (): string | null => localStorage.getItem('access_token'),
getRefresh: (): string | null => localStorage.getItem('refresh_token'),
setAccess: (token: string): void =>
localStorage.setItem('access_token', token),
setRefresh: (token: string): void =>
localStorage.setItem('refresh_token', token),
setTokens: (access: string, refresh: string): void => {
localStorage.setItem('access_token', access);
localStorage.setItem('refresh_token', refresh);
},
clear: (): void => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
},
};
// ─── Redirect helper ───────────────────────────────────────────────────────────
function redirectToMain(): void {
// Detect current locale from the URL path, fall back to DEFAULT_LOCALE
const pathLocale = window.location.pathname.split('/')[1];
const validLocales = ['uz', 'ru', 'en'];
const locale = validLocales.includes(pathLocale)
? pathLocale
: DEFAULT_LOCALE;
window.location.href = `/${locale}`;
}
// ─── Axios instance ────────────────────────────────────────────────────────────
const api = axios.create({
baseURL: baseUrl,
timeout: 15_000,
});
// ─── Flag to prevent multiple simultaneous refresh calls ──────────────────────
let isRefreshing = false;
// Queue of failed requests waiting for the new token
type FailedRequestResolver = (token: string) => void;
let failedQueue: {
resolve: FailedRequestResolver;
reject: (err: unknown) => void;
}[] = [];
function processQueue(error: unknown, token: string | null = null): void {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else if (token) {
resolve(token);
}
});
failedQueue = [];
}
// ─── Request interceptor — attach access token ────────────────────────────────
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = TokenStorage.getAccess();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
config.headers['Accept-Language'] = getRouteLang();
return config;
},
(error: AxiosError) => Promise.reject(error),
);
// ─── Response interceptor — handle 401, refresh, retry ───────────────────────
api.interceptors.response.use(
// ── 2xx: pass through unchanged ────────────────────────────────────────────
(response: AxiosResponse) => response,
// ── Error: attempt token refresh on 401 ────────────────────────────────────
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
const status = error.response?.status;
// Only attempt refresh on 401 and only once per request
if (status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
const refreshToken = TokenStorage.getRefresh();
// No refresh token available — clear everything and redirect
if (!refreshToken) {
TokenStorage.clear();
redirectToMain();
return Promise.reject(error);
}
// If a refresh is already in progress, queue this request
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(api(originalRequest));
},
reject,
});
});
}
// Mark that we're refreshing and this request has been retried
originalRequest._retry = true;
isRefreshing = true;
try {
// ── Call your refresh endpoint ──────────────────────────────────────────
// Adjust the URL and payload shape to match your backend.
const { data } = await axios.post<{
access_token: string;
refresh_token: string;
}>(
`${baseUrl}/auth/refresh`, // ← your refresh endpoint
{ refresh_token: refreshToken },
{ headers: { 'Accept-Language': getRouteLang() } },
);
const { access_token, refresh_token } = data;
TokenStorage.setTokens(access_token, refresh_token);
// Update the Authorization header for the retried request
originalRequest.headers.Authorization = `Bearer ${access_token}`;
// Unblock all queued requests with the new token
processQueue(null, access_token);
// Retry the original failed request
return api(originalRequest);
} catch (refreshError) {
// Refresh itself failed — clear tokens and redirect to home
processQueue(refreshError, null);
TokenStorage.clear();
redirectToMain();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
// ─── Public request function ───────────────────────────────────────────────────
export const apiRequest = async <T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
url: string,
data?: unknown,
config?: Omit<AxiosRequestConfig, 'method' | 'url' | 'data'>,
): Promise<AxiosResponse<T>> => {
// ← return full response
const response: AxiosResponse<T> = await api.request<T>({
const response = await api.request<T>({
method,
url,
data,
...config,
headers: {
'Accept-Language': getRouteLang(),
...config?.headers,
// Accept-Language is already set in the request interceptor,
// but config?.headers can still override it if needed.
},
});
console.log(response);
return response; // ← return response, not response.data
return response;
};

View File

@@ -2,4 +2,5 @@ export const links = {
login: '/users/login/',
register: '/users/register/',
plagiarismCheck: '/plagiarism/check/',
history: '/shared/documents/list/',
};

View File

@@ -6,14 +6,14 @@ interface User {
surname: string;
}
interface UserStore {
interface UserPlagiat {
user: User | null;
setUser: (user: User | null) => void;
clearUser: () => void;
getUser: () => User | null;
}
export const useUserStore = create<UserStore>((set, get) => ({
export const useUserPlagiatStore = create<UserPlagiat>((set, get) => ({
user: null,
setUser: (user: User | null) => set({ user }),
clearUser: () => set({ user: null }),

View File

@@ -8,6 +8,8 @@ import {
import { selectFullName, useUserStore } from './userStore';
import { isFormValid, validatePlagiarismForm } from './validation';
import { submitPlagiarismCheck } from '@/shared/request/plagiarismapi';
import { toast } from 'react-toastify';
import { useUserPlagiatStore } from '@/shared/zustand/user';
// ─── Initial States ──────────────────────────────────────────────────────────
@@ -27,7 +29,7 @@ const INITIAL_SUBMISSION: SubmissionState = {
export function usePlagiarismForm() {
const senderFullName = useUserStore(selectFullName);
const user = useUserPlagiatStore((state) => state.user);
const [form, setForm] = useState<PlagiarismFormState>(INITIAL_FORM);
const [errors, setErrors] = useState<PlagiarismFormErrors>({});
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
@@ -61,6 +63,11 @@ export function usePlagiarismForm() {
async (e: React.FormEvent) => {
e.preventDefault();
console.log('Form submitted user:', user); // Debugging log
if (user === null) {
toast.error('Iltimos, avval tizimga kiring!');
return;
}
// Run validation first
const validationErrors = validatePlagiarismForm(form);
if (!isFormValid(validationErrors)) {

View File

@@ -4,11 +4,19 @@ import { useTranslations } from 'next-intl';
import { useHistory } from '../lib/useHistory';
import { HistoryTable } from './historyTable';
import { Pagination } from './pagination';
import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
// ─── Page Header ───────────────────────────────────────────────────────────────
const PageHeader: React.FC = () => {
const t = useTranslations('HistoryPage');
const { data } = useQuery({
queryKey: ['history'],
queryFn: () => apiRequest('GET', links.history),
});
console.log('History data:', data); // Debugging log
return (
<div className="mb-6">

View File

@@ -12,7 +12,7 @@ import SubMenuLink from './SubMenuLink';
import { ChangeLang } from './ChangeLang';
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
import { useTranslations } from 'next-intl';
import { useUserStore } from '@/shared/zustand/user';
import { useUserPlagiatStore } from '@/shared/zustand/user';
function AuthButtons() {
const t = useTranslations('Navbar');
@@ -28,23 +28,34 @@ function AuthButtons() {
const toggleRegisterModal = useRegisterModal(
(state) => state.toggleRegisterModal,
);
const user = useUserStore((state) => state.user);
const user = useUserPlagiatStore((state) => state.user);
console.log('Current user:', user);
if (user) {
return (
<NavigationMenu>
<NavigationMenuItem>
<NavigationMenuTrigger>{user.name}</NavigationMenuTrigger>
<NavigationMenuContent className="bg-popover text-popover-foreground">
{userItem.map((subItem) => (
<NavigationMenuLink asChild key={subItem.title} className="w-80">
<SubMenuLink item={subItem} />
</NavigationMenuLink>
))}
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenu>
<div className="flex flex-row max-sm:items-center max-sm:justify-around gap-3">
<div className="sm:flex hidden">
<ChangeLang />
</div>
<NavigationMenu>
<NavigationMenuItem>
<NavigationMenuTrigger className="text-xl">
{user.name} {user.surname}
</NavigationMenuTrigger>
<NavigationMenuContent className="bg-popover text-popover-foreground">
{userItem.map((subItem) => (
<NavigationMenuLink
asChild
key={subItem.title}
className="w-80"
>
<SubMenuLink item={subItem} />
</NavigationMenuLink>
))}
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenu>
</div>
);
}