([]);
@@ -40,23 +130,28 @@ export default function ProblemsSet() {
const columns = useMemo(
() => [
- columnHelper.accessor((row) => row.status, {
- id: 'Status',
- cell: (info) => {
- let icon;
- if (user) {
- icon = isAccepted(info.row.original._id, user?.submissions) ? (
-
- ) : isRejected(info.row.original._id, user?.submissions) ? (
-
- ) : null;
- } else {
- icon = null;
- }
- return {icon}
;
- },
- filterFn: 'statusFilter' as any,
- }),
+ // Status column - only for logged in users
+ ...(isLogedIn
+ ? [
+ columnHelper.accessor((row) => row.status, {
+ id: 'Status',
+ cell: (info) => {
+ let icon;
+ if (user) {
+ icon = isAccepted(info.row.original._id, user?.submissions) ? (
+
+ ) : isRejected(info.row.original._id, user?.submissions) ? (
+
+ ) : null;
+ } else {
+ icon = null;
+ }
+ return {icon}
;
+ },
+ filterFn: 'statusFilter' as any,
+ }),
+ ]
+ : []),
columnHelper.accessor((row) => row.title, {
id: 'Title',
cell: (info) => {
@@ -66,45 +161,48 @@ export default function ProblemsSet() {
);
},
- filterFn: 'titleFilter' as any,
}),
columnHelper.accessor((row) => row.difficulty, {
id: 'Difficulty',
cell: (info) => {
return {capitalize(info.getValue())}
;
},
- filterFn: 'difficultyFilter' as any,
}),
],
- [user]
+ [user, isLogedIn]
);
+ const totalResultsValue = searchQuery.trim().length > 0
+ ? autocompleteTotal // Search results
+ : difficultyFilter !== 'all'
+ ? (difficultyFilteredData?.totalResults || 0) // Difficulty filtered
+ : (defaultProblemsData?.total || 0); // Default problems
+ const totalPages = Math.ceil(totalResultsValue / LIMIT);
+
+ // Handle pagination change from React Table
+ const handlePaginationChange = (updater: any) => {
+ setPaginationState((prev) =>
+ typeof updater === 'function' ? updater(prev) : updater
+ );
+ };
const table = useReactTable({
- data: problems ?? [],
+ data: tableData ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
+ // NOTE: Removed getPaginationRowModel() and getFilteredRowModel() for server-side pagination
+ // Server handles pagination and filtering, no need for client-side models
onColumnFiltersChange: setColumnFilters,
+ onPaginationChange: handlePaginationChange, // Use the handler that properly processes the updater
+ manualPagination: true, // Tell React Table pagination is handled server-side
+ pageCount: totalPages, // Calculate based on server total
state: {
columnFilters,
+ pagination: paginationState,
},
filterFns: {
- difficultyFilter: (row, columnId, filterValue) => {
- if (filterValue === 'all') {
- return row;
- }
- const column = columnId.toLowerCase();
- const value = filterValue ? row.original[column] === filterValue : row.original[column];
- return value;
- },
- titleFilter: (row, columnId, filterValue) => {
- const column = columnId.toLowerCase();
- console.log(row.original[column], filterValue);
- const value = row.original[column].toLowerCase().includes(filterValue.toLowerCase());
- return value;
- },
statusFilter: (row, _columnId, filterValue) => {
+ if (!isLogedIn) return true; // If not logged in, don't filter
+
const acceptedProblems = [
...new Set(user?.submissions.filter((s) => s.status === 'Accepted').map((s) => s.problemId)),
];
@@ -125,20 +223,20 @@ export default function ProblemsSet() {
});
const handleDifficultyChange = (event: SelectChangeEvent) => {
setDifficultyFilter(event.target.value);
- table.getColumn('Difficulty')?.setFilterValue(event.target.value);
+ // Difficulty is now handled server-side, no need to set table filter
};
const handleStatusChange = (event: SelectChangeEvent) => {
setStatusFilter(event.target.value);
table.getColumn('Status')?.setFilterValue(event.target.value);
};
- const handleQueryChange = (queryvalue: string) => {
- setSearchQuery(queryvalue);
- };
-
+ // Clear filters when search query is cleared
useEffect(() => {
- table.getColumn('Title')?.setFilterValue(debouncedSearchQuery);
- }, [debouncedSearchQuery]);
+ if (searchQuery.trim().length === 0) {
+ setStatusFilter('all');
+ table.getColumn('Status')?.setFilterValue('all');
+ }
+ }, [searchQuery]);
if (isLoading) {
return (
@@ -162,17 +260,15 @@ export default function ProblemsSet() {
statusFilter={statusFilter}
handleDifficultChange={handleDifficultyChange}
table={table}
- data={problems}
- searchQuery={searchQuery}
- handleQueryChange={handleQueryChange}
- clear={() => {
- setSearchQuery('');
- }}
+ data={tableData || []}
+ isTableLoading={isSearching || isDifficultyFiltering || isDefaultProblemsLoading}
+ isLogedIn={isLogedIn}
+ totalCount={totalResultsValue}
reset={() => {
- setSearchQuery('');
+ clearSearchQuery();
setStatusFilter('all');
setDifficultyFilter('all');
- table.getColumn('Difficulty')?.setFilterValue('all');
+ setPaginationState({ pageIndex: 0, pageSize: 10 }); // Reset to first page
table.getColumn('Status')?.setFilterValue('all');
}}
/>
diff --git a/src/components/Protected.tsx b/src/components/Protected.tsx
index a04c0b9..ebfc77d 100644
--- a/src/components/Protected.tsx
+++ b/src/components/Protected.tsx
@@ -1,3 +1,55 @@
-export default function Protected() {
- return Protected
;
+import { ComponentType, ReactNode } from 'react';
+import { Navigate } from 'react-router-dom';
+import { Box, CircularProgress } from '@mui/material';
+import { useUserSlice } from '../store/user';
+import { useAuthSlice } from '../store/authslice/auth';
+
+/**
+ * Higher-Order Component (HOC) for protecting routes
+ *
+ * Checks if user is authenticated before rendering component
+ * Redirects to login page if user is not logged in
+ * Shows loading state during session validation
+ */
+export function withProtected(
+ Component: ComponentType
,
+ fallbackRoute = '/signin'
+): (props: P) => ReactNode {
+ return (props: P) => {
+ const user = useUserSlice((state) => state.user);
+ const sessionLoading = useUserSlice((state) => state.sessionLoading);
+ const isLoggedIn = useAuthSlice((state) => state.isLogedIn);
+
+ // Wait for session validation to complete before rendering
+ if (sessionLoading === 'Loading') {
+ return (
+
+
+
+ );
+ }
+
+ const isAuthenticated = isLoggedIn && user?._id;
+
+ // Only redirect after session validation is COMPLETE
+ if (!isAuthenticated && sessionLoading !== 'Loading' && sessionLoading !== 'Not Started') {
+ return ;
+ }
+
+ return ;
+ };
}
+
diff --git a/src/components/route.tsx b/src/components/route.tsx
index 618495a..b414b44 100644
--- a/src/components/route.tsx
+++ b/src/components/route.tsx
@@ -3,6 +3,10 @@ import Home from './Pages/Home';
import Problem from './Pages/Problem/Index';
import SignIn from './Pages/SignIn/Index';
import SignUp from './Pages/SignUp/Index';
+import LeaderBoard from './Pages/LeaderBoard/LeaderBoard';
+import { withProtected } from './Protected';
+
+const ProtectedLeaderBoard = withProtected(LeaderBoard);
const router = createBrowserRouter([
{
@@ -21,5 +25,9 @@ const router = createBrowserRouter([
path: '/signup',
element: ,
},
+ {
+ path: '/leaderboard',
+ element: ,
+ },
]);
export default router;
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index eab5c05..b9af66a 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -2,9 +2,6 @@ import { createContext, FC, useContext, useEffect } from 'react';
import { authCtx, contextWrapperProps } from '../utils/types';
import { useAuthSlice } from '../store/authslice/auth';
import { useUserSlice } from '../store/user';
-import { useProblemSlice } from '../store/problemSlice/problem';
-import { useQuery } from '@tanstack/react-query';
-import getProblems from '../services/getProblems';
export const AuthContext = createContext({ isLoading: false, isError: false, error: null });
@@ -28,22 +25,12 @@ export const AuthContextWrapper: FC = ({ children }) => {
}
}
}, [sessionLoading]);
- const { data, isLoading, isError, error } = useQuery({
- queryKey: ['problems'],
- queryFn: getProblems,
- refetchOnWindowFocus: false,
- });
- const { setProblems } = useProblemSlice();
- useEffect(() => {
- if (data && data.data) {
- setProblems(data.data);
- }
- }, [data]);
+
useEffect(() => {
if (!['/signin', '/signup'].includes(window.location.pathname)) {
checkSession();
}
}, []);
- return {children};
+ return {children};
};
diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts
index b62a6cb..210bcd9 100644
--- a/src/hooks/useDebounce.ts
+++ b/src/hooks/useDebounce.ts
@@ -1,13 +1,19 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
export default function useDebounce(value: string, delay: number) {
const [debounceValue, setDebounceValue] = useState(value);
+ const timerRef=useRef | null>(null);
useEffect(() => {
- const timer = setTimeout(() => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ timerRef.current= setTimeout(() => {
setDebounceValue(value);
}, delay);
return () => {
- clearTimeout(timer);
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
};
}, [value, delay || 500]);
diff --git a/src/hooks/useProblemsAutocomplete.ts b/src/hooks/useProblemsAutocomplete.ts
new file mode 100644
index 0000000..8a3f75b
--- /dev/null
+++ b/src/hooks/useProblemsAutocomplete.ts
@@ -0,0 +1,71 @@
+/**
+ * Hook for managing autocomplete search with React Query
+ * Only accepts search query - difficulty filtering is separate
+ * Returns both top 5 results for dropdown and all results for table
+ */
+
+import { useQuery } from '@tanstack/react-query';
+import { searchProblems } from '../services/searchProblems';
+import useDebounce from './useDebounce';
+import { MappedSearchResult } from '../utils/elasticsearchMapper';
+
+interface UseProblemsAutocompleteReturn {
+ topResults: MappedSearchResult[];
+ allResults: MappedSearchResult[];
+ totalResults: number;
+ isSearching: boolean;
+ error: Error | null;
+}
+
+/**
+ * Manages autocomplete search with React Query
+ * - Single query with fast debounce (200ms)
+ * - Only accepts search query (no difficulty parameter)
+ * - Returns top 5 for autocomplete dropdown
+ * - Returns all results for table (client-side status filtering applied)
+ * - Supports pagination with page and limit
+ *
+ * Note: Difficulty filtering is handled separately - passed directly to searchProblems
+ * when needed for table results. This hook only handles query-based search.
+ *
+ * @param searchQuery - Raw search input from user
+ * @param page - Current page number (1-indexed)
+ * @param limit - Items per page (default: 10)
+ * @returns Object with topResults, allResults, loading state, and errors
+ */
+export const useProblemsAutocomplete = (
+ searchQuery: string,
+ page: number = 1,
+ limit: number = 10
+): UseProblemsAutocompleteReturn => {
+ // Fast debounce for search (200ms)
+ const debouncedQuery = useDebounce(searchQuery, 200);
+
+ // Single Query: Search results by query only (no difficulty)
+ const {
+ data: searchData,
+ isLoading: isSearching,
+ error,
+ } = useQuery({
+ queryKey: ['problems-search', { query: debouncedQuery.trim(), page, limit }],
+ queryFn: async ({ queryKey }) => {
+ // queryKey contains: ['problems-search', { query, page, limit }]
+ const { query: queryVal, page: pageVal, limit: limitVal } = queryKey[1] as { query: string; page: number; limit: number };
+ return searchProblems(pageVal, limitVal, queryVal);
+ },
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ gcTime: 10 * 60 * 1000, // 10 minutes garbage collection
+ enabled: debouncedQuery.trim().length > 0, // Only fetch when user types
+ refetchOnWindowFocus: false,
+ });
+
+ return {
+ topResults: searchData?.topResults || [],
+ allResults: searchData?.allResults || [],
+ totalResults: searchData?.totalResults || 0,
+ isSearching,
+ error,
+ };
+};
+
+export default useProblemsAutocomplete;
diff --git a/src/services/addSubmission.ts b/src/services/addSubmission.ts
index 51645f2..3bd145a 100644
--- a/src/services/addSubmission.ts
+++ b/src/services/addSubmission.ts
@@ -1,9 +1,10 @@
import { protectedapi } from '../API/Index';
import { submissionprops, updateUserType } from '../utils/types';
-const addSubmission = async (updateUserProps: { id: string; newsubmission: submissionprops }) => {
+//
+const runCode = async (updateUserProps: { id: string; newsubmission: submissionprops }) => {
try {
const response = await protectedapi.patch(
- `/users/${updateUserProps.id}/submission`,
+ `/users/${updateUserProps.id}/submission/${updateUserProps.newsubmission.submissionId}`,
updateUserProps.newsubmission
);
if (response.data.status === 'Failure') {
@@ -17,4 +18,4 @@ const addSubmission = async (updateUserProps: { id: string; newsubmission: submi
}
};
-export default addSubmission;
+export default runCode;
diff --git a/src/services/batchwiseSubmission.ts b/src/services/batchwiseSubmission.ts
index ce99e6d..d83d5b4 100644
--- a/src/services/batchwiseSubmission.ts
+++ b/src/services/batchwiseSubmission.ts
@@ -1,11 +1,11 @@
-import judgeapi from '../API/judge0';
-import { batchsubmission } from '../utils/types';
-async function batchwiseSubmission(problems: T) {
+import { protectedapi } from '../API/Index';
+import { batchSubmissionResponse } from '../utils/types';
+async function batchwiseSubmission(userId: string, problems: T) {
try {
- const response = await judgeapi.post(`/submissions/batch`, {
+ const response = await protectedapi.post(`users/${userId}/submissions/batch`, {
submissions: problems,
});
- return response.data;
+ return response.data.data;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
diff --git a/src/services/getLeaderBoardPagination.ts b/src/services/getLeaderBoardPagination.ts
new file mode 100644
index 0000000..118af1c
--- /dev/null
+++ b/src/services/getLeaderBoardPagination.ts
@@ -0,0 +1,55 @@
+import { protectedapi } from '../API/Index';
+import { commonresponse } from '../utils/types';
+
+export interface LeaderboardUser {
+ _id: string;
+ userId: string;
+ userName: string;
+ totalPoints: number;
+ easyProblems: number;
+ mediumProblems: number;
+ hardProblems: number;
+ totalSolved: number;
+ currentRank: number;
+ previousRank: number;
+ isOnline: boolean;
+ lastUpdated: Date;
+}
+
+export interface LeaderboardResponse extends Omit {
+ data: {
+ users: LeaderboardUser[];
+ pagination: {
+ page: number;
+ limit: number;
+ totalUsers: number;
+ totalPages: number;
+ hasNextPage: boolean;
+ hasPrevPage: boolean;
+ };
+ };
+}
+
+const getLeaderBoardPagination = async (page: number = 1, limit: number = 50): Promise => {
+ try {
+ const response = await protectedapi.get(`/leaderboard/paginated`, {
+ params: {
+ page,
+ limit,
+ },
+ });
+
+ if (response.data.status === 'Failure') {
+ throw new Error(response.data.error || 'Failed to fetch leaderboard');
+ }
+
+ return response.data;
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(error.message);
+ }
+ throw new Error('Failed to fetch leaderboard');
+ }
+};
+
+export default getLeaderBoardPagination;
diff --git a/src/services/getLeaderboardFilters.ts b/src/services/getLeaderboardFilters.ts
new file mode 100644
index 0000000..4724938
--- /dev/null
+++ b/src/services/getLeaderboardFilters.ts
@@ -0,0 +1,75 @@
+import { protectedapi } from '../API/Index';
+import { LeaderboardUser, LeaderboardPagination } from '../utils/types';
+
+export interface LeaderboardFiltersParams {
+ userName?: string;
+ period?: 'all' | 'week' | 'month' | 'today';
+ page?: number;
+ limit?: number;
+}
+
+export interface LeaderboardFiltersResponse {
+ users: LeaderboardUser[];
+ pagination?: LeaderboardPagination;
+}
+
+/**
+ * Fetch leaderboard data with filters applied
+ * Supports search by username, filter by time period, and pagination
+ *
+ * @param params - Filter params (userName, period, page, limit)
+ * @returns Filtered leaderboard data with pagination
+ *
+ * @example
+ * const data = await getLeaderboardFilters({
+ * userName: 'john',
+ * period: 'week',
+ * page: 1,
+ * limit: 50
+ * });
+ */
+export async function getLeaderboardFilters(
+ params: LeaderboardFiltersParams
+): Promise {
+ try {
+ const queryParams = new URLSearchParams();
+
+ if (params.userName && params.userName.trim()) {
+ queryParams.append('userName', params.userName.trim());
+ }
+ if (params.period && params.period !== 'all') {
+ queryParams.append('period', params.period);
+ }
+ if (params.page) {
+ queryParams.append('page', params.page.toString());
+ }
+ if (params.limit) {
+ queryParams.append('limit', params.limit.toString());
+ }
+
+ const response = await protectedapi.get<{
+ status: 'Success' | 'Failure';
+ data: {
+ users: LeaderboardUser[];
+ pagination?: LeaderboardPagination;
+ };
+ message?: string;
+ }>(
+ `/leaderboard/filters${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
+ );
+
+ if (response.data.status === 'Success' && response.data.data) {
+ return {
+ users: Array.isArray(response.data.data.users) ? response.data.data.users : [],
+ pagination: response.data.data.pagination,
+ };
+ }
+
+ throw new Error(response.data.message || 'Failed to fetch filtered leaderboard');
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`[getLeaderboardFilters] ${error.message}`);
+ }
+ throw error;
+ }
+}
diff --git a/src/services/getProblems.ts b/src/services/getProblems.ts
index f8b4550..92c90fa 100644
--- a/src/services/getProblems.ts
+++ b/src/services/getProblems.ts
@@ -1,12 +1,29 @@
import api from '../API/Index';
-import { getProblemsType } from '../utils/types';
-const getProblems = async () => {
+
+/**
+ * Get all problems with optional pagination
+ * @param page - Page number (1-indexed)
+ * @param limit - Items per page
+ */
+const getProblems = async (page: number = 1, limit: number = 10) => {
try {
- const response = await api.get('/problems');
+ const response = await api.get<{
+ status: string;
+ data: {
+ problems: any[];
+ total: number;
+ };
+ }>(`/problems?page=${page}&limit=${limit}`);
+
if (response.data.status === 'Failure') {
- throw new Error(response.data.error);
+ throw new Error('Failed to fetch problems');
}
- return response.data;
+
+ return {
+ problems: response.data.data.problems || [],
+ total: response.data.data.total || 0,
+ status: response.data.status,
+ };
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message);
diff --git a/src/services/getSubmissionStatus.ts b/src/services/getSubmissionStatus.ts
index fb9ec5b..1fe50f5 100644
--- a/src/services/getSubmissionStatus.ts
+++ b/src/services/getSubmissionStatus.ts
@@ -11,7 +11,7 @@ export default async function getStatus(submissionId: string) {
};
stdout: null | string;
stderr: null | string;
- }>(`/submissions/${submissionId}?fields=stdout,stderr,language_id,stdin,status,expected_output`);
+ }>(`/submissions/${submissionId}?fields=stdout,stderr,language_id,stdin,status,expected_output,memory,time`);
return response.data;
} catch (error) {
if (error instanceof Error) {
diff --git a/src/services/retryToken.ts b/src/services/retryToken.ts
index 548feea..e036dbd 100644
--- a/src/services/retryToken.ts
+++ b/src/services/retryToken.ts
@@ -1,5 +1,6 @@
import { protectedapi } from '../API/Index';
import { refreshTokenRes } from '../utils/types';
+
const refreshToken = async () => {
try {
const response = await protectedapi.post('/auth/refresh');
@@ -8,9 +9,12 @@ const refreshToken = async () => {
}
return response.data;
} catch (error) {
+ // Log the error for debugging
+ console.error('[refreshToken] Failed to refresh token:', error);
if (error instanceof Error) {
throw new Error(error.message);
}
+ throw new Error('Token refresh failed');
}
};
diff --git a/src/services/searchProblems.ts b/src/services/searchProblems.ts
new file mode 100644
index 0000000..0d1e7de
--- /dev/null
+++ b/src/services/searchProblems.ts
@@ -0,0 +1,45 @@
+import api from "../API/Index";
+import { parseElasticsearchResponse, SearchResultHit } from "../utils/elasticsearchMapper";
+
+export const searchProblems = async (page: number, limit: number, query?: string, difficulty?: string) => {
+ try {
+ // Allow API call if either query exists OR difficulty filter is set (not 'all')
+ if ((!query || query.trim().length === 0) && (!difficulty || difficulty === 'all')) {
+ // Return empty arrays only if both query is empty AND difficulty is 'all'
+ return {
+ topResults: [],
+ allResults: [],
+ totalResults: 0,
+ };
+ }
+
+ const params = new URLSearchParams();
+ params.append('page', page.toString());
+ params.append('limit', limit.toString());
+ if (query && query.trim().length > 0) {
+ params.append('query', query);
+ }
+
+ // Add difficulty to query params if provided and not 'all'
+ if (difficulty && difficulty !== 'all') {
+ params.append('difficulty', difficulty);
+ }
+
+ const response = await api.get(`problems/search/filters?${params.toString()}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // The response from ES is already wrapped by backend
+ // Extract the hits from the response
+ const hits: SearchResultHit[] = response.data?.data.results || [];
+ const total: number = response.data?.data.total || 0;
+ // Parse and return both top 5 and all results
+ return parseElasticsearchResponse(hits,total);
+ } catch (error) {
+ console.error('Error searching problems:', error);
+ throw error;
+ }
+};
\ No newline at end of file
diff --git a/src/services/sumbitCode.ts b/src/services/sumbitCode.ts
index ffdf577..1bcc05e 100644
--- a/src/services/sumbitCode.ts
+++ b/src/services/sumbitCode.ts
@@ -1,17 +1,19 @@
-import judgeapi from '../API/judge0';
+import { protectedapi } from '../API/Index';
interface submitCodeArgs {
code: string;
language_id: number;
- input: string;
+ stdin: string;
expected_output: string;
+ userId: string;
}
async function submitCode(params: submitCodeArgs) {
try {
- const response = await judgeapi.post('/submissions', {
+ const response = await protectedapi.post(`users/${params.userId}/submissions`, {
source_code: params.code,
language_id: params.language_id,
- stdin: params.input.replace('\\n', '\n'),
+ stdin: params?.stdin?.replace('\\n', '\n'),
expected_output: params.expected_output,
+ userId: params.userId,
});
return response;
} catch (error) {
diff --git a/src/services/updateSubmission.ts b/src/services/updateSubmission.ts
index e227de4..aaed184 100644
--- a/src/services/updateSubmission.ts
+++ b/src/services/updateSubmission.ts
@@ -1,9 +1,13 @@
import { protectedapi } from '../API/Index';
-import { updateuser, updateUserType } from '../utils/types';
-const updateSubmission = async (updateUserProps: { id: string; updateduser: updateuser }) => {
+import { IupdateSubmission, updateUserType } from '../utils/types';
+const updateSubmission = async (updateUserProps: {
+ submissionId: string;
+ userId: string;
+ updateduser: IupdateSubmission;
+}) => {
try {
- const response = await protectedapi.patch(
- `/users/${updateUserProps.id}`,
+ const response = await protectedapi.put(
+ `/users/${updateUserProps.userId}/submissions/${updateUserProps.submissionId}`,
updateUserProps.updateduser
);
if (response.data.status === 'Failure') {
diff --git a/src/store/authslice/auth.tsx b/src/store/authslice/auth.tsx
index ce91ee2..f5cc095 100644
--- a/src/store/authslice/auth.tsx
+++ b/src/store/authslice/auth.tsx
@@ -1,4 +1,5 @@
import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
interface authSlice {
isLogedIn: boolean;
@@ -6,8 +7,16 @@ interface authSlice {
signIn: () => void;
}
-export const useAuthSlice = create()((set) => ({
- isLogedIn: false,
- signOut: () => set(() => ({ isLogedIn: false })),
- signIn: () => set(() => ({ isLogedIn: true })),
-}));
+export const useAuthSlice = create()(
+ persist(
+ (set) => ({
+ isLogedIn: false,
+ signOut: () => set(() => ({ isLogedIn: false })),
+ signIn: () => set(() => ({ isLogedIn: true })),
+ }),
+ {
+ name: 'auth-storage', // localStorage key
+ partialize: (state) => ({ isLogedIn: state.isLogedIn }), // Only persist isLogedIn
+ }
+ )
+);
diff --git a/src/store/index.tsx b/src/store/index.tsx
index e69de29..760e3e0 100644
--- a/src/store/index.tsx
+++ b/src/store/index.tsx
@@ -0,0 +1,8 @@
+// Re-export all store slices for centralized access
+export { useAuthSlice } from './authslice/auth';
+export { useUserSlice } from './user';
+export { useProblemSlice } from './problemSlice/problem';
+export { useLeaderboardStore } from './leaderboardSlice/leaderboard';
+
+// Export types
+export type { LeaderboardState } from '../utils/types';
diff --git a/src/store/leaderboardSlice/index.ts b/src/store/leaderboardSlice/index.ts
new file mode 100644
index 0000000..72dc535
--- /dev/null
+++ b/src/store/leaderboardSlice/index.ts
@@ -0,0 +1,429 @@
+import { create } from 'zustand';
+import {
+ LeaderboardState,
+ LeaderboardUser,
+ LeaderboardFilters,
+ TimePeriod,
+ DifficultyFilter,
+ SortBy,
+ SortOrder,
+ ViewMode,
+ UpdateEvent,
+} from '../../utils/types';
+
+interface LeaderboardActions {
+ // Data actions
+ setLeaderboardUsers: (users: LeaderboardUser[], lastFetched: Date) => void;
+ setLoading: (isLoading: boolean) => void;
+ setError: (error: string | null) => void;
+ clearError: () => void;
+
+ // Pagination actions
+ setCurrentPage: (page: number) => void;
+ nextPage: () => void;
+ prevPage: () => void;
+ goToFirstPage: () => void;
+ goToLastPage: () => void;
+ setPageSize: (size: number) => void;
+ setPaginationState: (pagination: Partial>) => void;
+
+ // Filter actions
+ setTimePeriod: (period: TimePeriod) => void;
+ setSearchQuery: (query: string) => void;
+ setDifficultyFilter: (difficulty: DifficultyFilter) => void;
+ toggleOnlineOnly: () => void;
+ clearFilters: () => void;
+ applyFilters: (filters: Partial) => void;
+
+ // User actions
+ setCurrentUser: (userId: string | null) => void;
+ setCurrentUserRank: (rank: number | null) => void;
+ highlightUser: (userId: string | null) => void;
+
+ // UI actions
+ setViewMode: (mode: ViewMode) => void;
+ setSortBy: (sortBy: SortBy) => void;
+ setSortOrder: (order: SortOrder) => void;
+ toggleAutoRefresh: () => void;
+ setRefreshInterval: (seconds: number) => void;
+ setShowRankIndicators: (show: boolean) => void;
+ toggleShowRankIndicators: () => void;
+
+ // Cache actions
+ cachePage: (pageNumber: number, data: LeaderboardUser[]) => void;
+ getCachedPage: (pageNumber: number) => LeaderboardUser[] | null;
+ clearCache: () => void;
+ invalidateCache: () => void;
+
+ // Realtime actions
+ setRealtimeConnected: (connected: boolean) => void;
+ updateRealtimeData: (updates: UpdateEvent[]) => void;
+ clearPendingUpdates: () => void;
+ setNotificationsEnabled: (enabled: boolean) => void;
+
+ // Batch actions
+ resetLeaderboard: () => void;
+}
+
+interface LeaderboardComputed {
+ rankedUsers: () => LeaderboardUser[];
+ topThreeUsers: () => LeaderboardUser[];
+ filteredUsers: () => LeaderboardUser[];
+ visibleUsers: () => LeaderboardUser[];
+ getCurrentUserStats: () => LeaderboardUser | null;
+ getUserById: (userId: string) => LeaderboardUser | null;
+ getNearbyRanks: (rank: number, range: number) => LeaderboardUser[];
+ getUserRankChange: (userId: string) => number;
+ getUserPercentile: (userId: string) => number;
+ isUserInTopTen: (userId: string) => boolean;
+ isDataStale: () => boolean;
+ searchResults: () => LeaderboardUser[];
+}
+
+const INITIAL_STATE: LeaderboardState = {
+ leaderboardData: {
+ users: [],
+ isLoading: false,
+ error: null,
+ lastFetched: null,
+ },
+ pagination: {
+ currentPage: 1,
+ totalPages: 1,
+ pageSize: 50,
+ totalUsers: 0,
+ hasNextPage: false,
+ hasPrevPage: false,
+ },
+ filters: {
+ timePeriod: 'all',
+ searchQuery: '',
+ difficultyFilter: 'all',
+ showOnlineOnly: false,
+ },
+ currentUserId: null,
+ currentUserRank: null,
+ ui: {
+ viewMode: 'table',
+ sortBy: 'rank',
+ sortOrder: 'asc',
+ highlightedUserId: null,
+ autoRefresh: false,
+ refreshInterval: 30,
+ showRankIndicators: true,
+ },
+ realtime: {
+ isConnected: false,
+ lastUpdate: null,
+ pendingUpdates: [],
+ notificationsEnabled: true,
+ },
+ cache: {
+ pageCache: new Map(),
+ lastCacheCleared: null,
+ cacheDuration: 5 * 60 * 1000,
+ },
+};
+
+export const useLeaderboardStore = create()(
+ (set, get) => ({
+ ...INITIAL_STATE,
+
+ setLeaderboardUsers: (users, lastFetched) => {
+ set((state) => ({
+ leaderboardData: {
+ ...state.leaderboardData,
+ users,
+ lastFetched,
+ isLoading: false,
+ },
+ pagination: {
+ ...state.pagination,
+ totalUsers: users.length,
+ },
+ }));
+ },
+
+ setLoading: (isLoading) =>
+ set((state) => ({
+ leaderboardData: { ...state.leaderboardData, isLoading },
+ })),
+
+ setError: (error) =>
+ set((state) => ({
+ leaderboardData: { ...state.leaderboardData, error },
+ })),
+
+ clearError: () =>
+ set((state) => ({
+ leaderboardData: { ...state.leaderboardData, error: null },
+ })),
+
+ setCurrentPage: (page) =>
+ set((state) => ({
+ pagination: {
+ ...state.pagination,
+ currentPage: Math.max(1, Math.min(page, state.pagination.totalPages)),
+ hasPrevPage: page > 1,
+ hasNextPage: page < state.pagination.totalPages,
+ },
+ })),
+
+ nextPage: () => {
+ const state = get();
+ if (state.pagination.hasNextPage) {
+ state.setCurrentPage(state.pagination.currentPage + 1);
+ }
+ },
+
+ prevPage: () => {
+ const state = get();
+ if (state.pagination.hasPrevPage) {
+ state.setCurrentPage(state.pagination.currentPage - 1);
+ }
+ },
+
+ goToFirstPage: () => get().setCurrentPage(1),
+
+ goToLastPage: () => get().setCurrentPage(get().pagination.totalPages),
+
+ setPageSize: (size) =>
+ set((state) => ({
+ pagination: {
+ ...state.pagination,
+ pageSize: size,
+ currentPage: 1,
+ totalPages: Math.ceil(state.pagination.totalUsers / size),
+ },
+ })),
+
+ setPaginationState: (pagination) =>
+ set((state) => ({
+ pagination: { ...state.pagination, ...pagination },
+ })),
+
+ setTimePeriod: (period) =>
+ set((state) => ({
+ filters: { ...state.filters, timePeriod: period },
+ })),
+
+ setSearchQuery: (query) =>
+ set((state) => ({
+ filters: { ...state.filters, searchQuery: query },
+ })),
+
+ setDifficultyFilter: (difficulty) =>
+ set((state) => ({
+ filters: { ...state.filters, difficultyFilter: difficulty },
+ })),
+
+ toggleOnlineOnly: () =>
+ set((state) => ({
+ filters: {
+ ...state.filters,
+ showOnlineOnly: !state.filters.showOnlineOnly,
+ },
+ })),
+
+ clearFilters: () =>
+ set({
+ filters: {
+ timePeriod: 'all',
+ searchQuery: '',
+ difficultyFilter: 'all',
+ showOnlineOnly: false,
+ },
+ }),
+
+ applyFilters: (filters) =>
+ set((state) => ({
+ filters: { ...state.filters, ...filters },
+ pagination: { ...state.pagination, currentPage: 1 },
+ })),
+
+ setCurrentUser: (userId) => set({ currentUserId: userId }),
+
+ setCurrentUserRank: (rank) => set({ currentUserRank: rank }),
+
+ highlightUser: (userId) =>
+ set((state) => ({
+ ui: { ...state.ui, highlightedUserId: userId },
+ })),
+
+ setViewMode: (mode) =>
+ set((state) => ({
+ ui: { ...state.ui, viewMode: mode },
+ })),
+
+ setSortBy: (sortBy) =>
+ set((state) => ({
+ ui: { ...state.ui, sortBy },
+ })),
+
+ setSortOrder: (order) =>
+ set((state) => ({
+ ui: { ...state.ui, sortOrder: order },
+ })),
+
+ toggleAutoRefresh: () =>
+ set((state) => ({
+ ui: { ...state.ui, autoRefresh: !state.ui.autoRefresh },
+ })),
+
+ setRefreshInterval: (seconds) =>
+ set((state) => ({
+ ui: { ...state.ui, refreshInterval: seconds },
+ })),
+
+ setShowRankIndicators: (show) =>
+ set((state) => ({
+ ui: { ...state.ui, showRankIndicators: show },
+ })),
+
+ toggleShowRankIndicators: () =>
+ set((state) => ({
+ ui: { ...state.ui, showRankIndicators: !state.ui.showRankIndicators },
+ })),
+
+ cachePage: (pageNumber, data) =>
+ set((state) => {
+ const newPageCache = new Map(state.cache.pageCache);
+ newPageCache.set(pageNumber, data);
+ return {
+ cache: { ...state.cache, pageCache: newPageCache },
+ };
+ }),
+
+ getCachedPage: (pageNumber) => {
+ const state = get();
+ const cached = state.cache.pageCache.get(pageNumber);
+ if (!cached) return null;
+ const lastCleared = state.cache.lastCacheCleared;
+ if (lastCleared && Date.now() - lastCleared.getTime() > state.cache.cacheDuration) {
+ return null;
+ }
+ return cached;
+ },
+
+ clearCache: () =>
+ set((state) => ({
+ cache: {
+ ...state.cache,
+ pageCache: new Map(),
+ lastCacheCleared: new Date(),
+ },
+ })),
+
+ invalidateCache: () => {
+ get().clearCache();
+ },
+
+ setRealtimeConnected: (connected) =>
+ set((state) => ({
+ realtime: { ...state.realtime, isConnected: connected },
+ })),
+
+ updateRealtimeData: (updates) =>
+ set((state) => ({
+ realtime: {
+ ...state.realtime,
+ pendingUpdates: [...state.realtime.pendingUpdates, ...updates],
+ lastUpdate: new Date(),
+ },
+ })),
+
+ clearPendingUpdates: () =>
+ set((state) => ({
+ realtime: { ...state.realtime, pendingUpdates: [] },
+ })),
+
+ setNotificationsEnabled: (enabled) =>
+ set((state) => ({
+ realtime: { ...state.realtime, notificationsEnabled: enabled },
+ })),
+
+ resetLeaderboard: () => set(INITIAL_STATE),
+
+ rankedUsers: () => {
+ const state = get();
+ return (state?.leaderboardData.users ?? []).filter((u) => u.totalPoints > 0);
+ },
+
+ topThreeUsers: () => {
+ const state = get();
+ return (state?.leaderboardData.users ?? []).slice(0, 3);
+ },
+
+ filteredUsers: () => {
+ // Filtering is now handled by backend API
+ // This returns users as-is from the API response
+ const state = get();
+ return state.leaderboardData.users ?? [];
+ },
+
+ visibleUsers: () => {
+ const state = get();
+ const filtered = get().filteredUsers() ?? [];
+ const start = (state.pagination.currentPage - 1) * state.pagination.pageSize;
+ const end = start + state.pagination.pageSize;
+ return filtered.slice(start, end);
+ },
+
+ getCurrentUserStats: () => {
+ const state = get();
+ if (!state.currentUserId) return null;
+ return state?.leaderboardData.users?.find((u) => u.userId === state.currentUserId) || null;
+ },
+
+ getUserById: (userId: string) => {
+ const state = get();
+ return state?.leaderboardData.users?.find((u) => u.userId === userId) || null;
+ },
+
+ getNearbyRanks: (rank: number, range: number = 5) => {
+ const state = get();
+ const users = state.leaderboardData.users ?? [];
+ const start = Math.max(0, rank - range - 1);
+ const end = Math.min(users.length, rank + range);
+ return users.slice(start, end);
+ },
+
+ getUserRankChange: (userId: string) => {
+ const user = get().getUserById(userId);
+ if (!user) return 0;
+ return user.previousRank - user.currentRank;
+ },
+
+ getUserPercentile: (userId: string) => {
+ const state = get();
+ const users = state.leaderboardData.users ?? [];
+ const user = users.find((u) => u.userId === userId);
+ if (!user || users.length === 0) return 0;
+ const userIndex = users.indexOf(user);
+ return (userIndex / users.length) * 100;
+ },
+
+ isUserInTopTen: (userId: string) => {
+ const state = get();
+ const users = state.leaderboardData.users ?? [];
+ const user = users.find((u) => u.userId === userId);
+ return user ? user.currentRank <= 10 : false;
+ },
+
+ isDataStale: () => {
+ const state = get();
+ if (!state.leaderboardData.lastFetched) return true;
+ const fiveMinutes = 5 * 60 * 1000;
+ return Date.now() - state.leaderboardData.lastFetched.getTime() > fiveMinutes;
+ },
+
+ searchResults: () => {
+ // Search is now handled by backend API via /leaderboard/filters endpoint
+ // This returns the already-filtered results from the API
+ const state = get();
+ return state.leaderboardData.users ?? [];
+ },
+ })
+);
+
+export default useLeaderboardStore;
diff --git a/src/store/leaderboardSlice/leaderboard.ts b/src/store/leaderboardSlice/leaderboard.ts
new file mode 100644
index 0000000..7b565de
--- /dev/null
+++ b/src/store/leaderboardSlice/leaderboard.ts
@@ -0,0 +1,2 @@
+// Re-export the useLeaderboardStore from index.ts (store implementation)
+export { useLeaderboardStore } from './index';
diff --git a/src/store/problemsSearchSlice/index.ts b/src/store/problemsSearchSlice/index.ts
new file mode 100644
index 0000000..6373172
--- /dev/null
+++ b/src/store/problemsSearchSlice/index.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand';
+
+interface ProblemsSearchState {
+ searchQuery: string;
+ setSearchQuery: (query: string) => void;
+ clearSearchQuery: () => void;
+}
+
+export const useProblemsSearchSlice = create((set) => ({
+ searchQuery: '',
+ setSearchQuery: (query: string) => set({ searchQuery: query }),
+ clearSearchQuery: () => set({ searchQuery: '' }),
+}));
diff --git a/src/utils/elasticsearchMapper.ts b/src/utils/elasticsearchMapper.ts
new file mode 100644
index 0000000..8a17d0f
--- /dev/null
+++ b/src/utils/elasticsearchMapper.ts
@@ -0,0 +1,62 @@
+/**
+ * Elasticsearch Response Parser & Mapper
+ * Transforms ES search results into typed format for UI consumption
+ */
+
+export interface SearchResultHit {
+ _index: string;
+ _id: string;
+ _score: number;
+ _source: {
+ title: string;
+ description?: string;
+ difficulty?: 'easy' | 'medium' | 'hard';
+ };
+}
+
+export interface MappedSearchResult {
+ id: string;
+ title: string;
+ score: number;
+ difficulty?: 'easy' | 'medium' | 'hard';
+ description?: string;
+}
+
+/**
+ * Maps Elasticsearch hit object to MappedSearchResult
+ * @param hit - ES search hit
+ * @returns Mapped result with proper types
+ */
+const mapHitToResult = (hit: SearchResultHit): MappedSearchResult => ({
+ id: hit._id,
+ title: hit._source.title,
+ score: hit._score,
+ difficulty: hit._source.difficulty,
+ description: hit._source.description,
+});
+
+/**
+ * Parses Elasticsearch search response
+ * Returns both top 5 results (for dropdown) and all results (for table)
+ * @param hits - Array of ES search hits
+ * @returns Object with topResults (5) and allResults (all)
+ */
+export const parseElasticsearchResponse = (
+ hits: SearchResultHit[],
+ total: number
+): {
+ topResults: MappedSearchResult[];
+ allResults: MappedSearchResult[];
+ totalResults: number;
+} => {
+ // Map and sort by score (ES already returns sorted, but we ensure it)
+ const mappedResults = hits
+ .map(mapHitToResult)
+ .sort((a, b) => b.score - a.score);
+
+ return {
+ topResults: mappedResults.slice(0, 5), // Top 5 for dropdown
+ allResults: mappedResults, // All for table
+ totalResults: total, // Total hits for pagination
+ };
+};
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 956ea8e..663295e 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -59,6 +59,13 @@ export interface validateSessionRes extends Omit {
data: { user: user | null; isExipred: boolean };
}
+export interface batchSubmissionResponse extends Omit {
+ data: {
+ submissionIds: string[];
+ _id: string;
+ };
+}
+
export interface metadata {
input_format: string;
output_format: string;
@@ -102,7 +109,17 @@ export interface user {
submissions: problemsubmission[];
}
export interface createUser extends Partial {}
-export interface updateuser extends Partial {}
+export interface IupdateSubmission {
+ submissionId: string;
+ status: string;
+ actual_output?: string[];
+ memoryUsed?: number[];
+ executionTime?: number[];
+ problemId: string;
+ languageId: number;
+ submittedAt?: Date;
+ difficulty?: string;
+}
export type status = 'Accepted' | 'Wrong Answer' | 'Processing';
export interface submissionprops {
problemId: string;
@@ -140,3 +157,105 @@ export interface ShrinkState {
}
export interface SavedProblems extends Pick {}
+
+// Leaderboard Types
+export interface LeaderboardUser {
+ _id: string;
+ userId: string;
+ userName: string;
+ totalPoints: number;
+ easyProblems: number;
+ mediumProblems: number;
+ hardProblems: number;
+ totalSolved: number;
+ currentRank: number;
+ previousRank: number;
+ isOnline: boolean;
+ lastUpdated: Date;
+}
+
+export interface LeaderboardPagination {
+ currentPage: number;
+ totalPages: number;
+ pageSize: number;
+ totalUsers: number;
+ hasNextPage: boolean;
+ hasPrevPage: boolean;
+}
+
+export type TimePeriod = 'all' | 'week' | 'month' | 'today';
+export type DifficultyFilter = 'all' | 'easy' | 'medium' | 'hard';
+export type ViewMode = 'table' | 'cards';
+export type SortBy = 'rank' | 'points' | 'recent';
+export type SortOrder = 'asc' | 'desc';
+
+export interface LeaderboardFilters {
+ timePeriod: TimePeriod;
+ searchQuery: string;
+ difficultyFilter: DifficultyFilter;
+ showOnlineOnly: boolean;
+}
+
+export interface LeaderboardUI {
+ viewMode: ViewMode;
+ sortBy: SortBy;
+ sortOrder: SortOrder;
+ highlightedUserId: string | null;
+ autoRefresh: boolean;
+ refreshInterval: number;
+ showRankIndicators: boolean;
+}
+
+export interface UpdateEvent {
+ userId: string;
+ type: 'rank_change' | 'points_update' | 'status_change';
+ data: Record;
+ timestamp: Date;
+}
+
+export interface LeaderboardRealtimeState {
+ isConnected: boolean;
+ lastUpdate: Date | null;
+ pendingUpdates: UpdateEvent[];
+ notificationsEnabled: boolean;
+}
+
+export interface LeaderboardCache {
+ pageCache: Map;
+ lastCacheCleared: Date | null;
+ cacheDuration: number;
+}
+
+export interface LeaderboardData {
+ users: LeaderboardUser[];
+ isLoading: boolean;
+ error: string | null;
+ lastFetched: Date | null;
+}
+
+export interface LeaderboardState {
+ // Data
+ leaderboardData: LeaderboardData;
+
+ // Pagination
+ pagination: LeaderboardPagination;
+
+ // Filters
+ filters: LeaderboardFilters;
+
+ // Current user
+ currentUserId: string | null;
+ currentUserRank: number | null;
+
+ // UI
+ ui: LeaderboardUI;
+
+ // Realtime
+ realtime: LeaderboardRealtimeState;
+
+ // Cache
+ cache: LeaderboardCache;
+}
+export interface SuccessResponse extends Omit {
+data:LeaderboardUser[]
+}
\ No newline at end of file