diff --git a/app/api/mht-cet/state-cutoffs/route.ts b/app/api/mht-cet/state-cutoffs/route.ts index 63ba1c0..2451aa8 100644 --- a/app/api/mht-cet/state-cutoffs/route.ts +++ b/app/api/mht-cet/state-cutoffs/route.ts @@ -1,20 +1,21 @@ import { NextRequest, NextResponse } from 'next/server'; import { getPocketBase, ensureAuthenticatedServer } from '@/lib/pocketbaseClient'; -export async function GET(request: NextRequest) { +export async function POST(request: NextRequest) { try { - console.log('API Route called with URL:', request.url); - - const { searchParams } = new URL(request.url); - const page = parseInt(searchParams.get('page') || '1'); - const perPage = parseInt(searchParams.get('perPage') || '50'); - const search = searchParams.get('search') || ''; - const categories = searchParams.get('categories') || ''; - const seatAllocations = searchParams.get('seatAllocations') || ''; - const courses = searchParams.get('courses') || ''; - const percentileInput = searchParams.get('percentileInput') || ''; - const sortBy = searchParams.get('sortBy') || 'last_rank'; - const sortOrder = searchParams.get('sortOrder') || 'desc'; + const body = await request.json(); + console.log('API Route called with body:', body); + + + const page = parseInt(body.page || '1'); + const perPage = parseInt(body.perPage || '50'); + const search = body.search || ''; + const categories = body.categories || []; + const seatAllocations = body.seatAllocations || []; + const courses = body.courses || []; + const percentileInput = body.percentileInput || ''; + const sortBy = body.sortBy || 'last_rank'; + const sortOrder = body.sortOrder || 'desc'; const pb = getPocketBase(); @@ -84,19 +85,13 @@ export async function GET(request: NextRequest) { } // Apply category filter if provided - if (categories) { - const categoryList = categories.split(',').filter(c => c.trim()); - if (categoryList.length > 0) { - mockData = mockData.filter(item => categoryList.includes(item.category)); - } + if (categories && categories.length > 0) { + mockData = mockData.filter((item: any) => categories.includes(item.category)); } // Apply seat allocation filter if provided - if (seatAllocations) { - const seatList = seatAllocations.split(',').filter(s => s.trim()); - if (seatList.length > 0) { - mockData = mockData.filter(item => seatList.includes(item.seat_allocation_section)); - } + if (seatAllocations && seatAllocations.length > 0) { + mockData = mockData.filter((item: any) => seatAllocations.includes(item.seat_allocation_section)); } // Apply percentile filter if provided @@ -105,23 +100,23 @@ export async function GET(request: NextRequest) { const minPercentile = Math.max(0, Math.round((targetPercentile - 1) * 10000000000) / 10000000000); const maxPercentile = Math.round(targetPercentile * 10000000000) / 10000000000; - mockData = mockData.filter(item => { + mockData = mockData.filter((item: any) => { const score = parseFloat(item.cutoff_score); return score >= minPercentile && score <= maxPercentile; }); // Sort by cutoff_score descending for percentile searches - mockData.sort((a, b) => parseFloat(b.cutoff_score) - parseFloat(a.cutoff_score)); + mockData.sort((a: any, b: any) => parseFloat(b.cutoff_score) - parseFloat(a.cutoff_score)); } else { // Apply other sorting when no percentile filter if (sortBy === 'cutoff_score') { - mockData.sort((a, b) => { + mockData.sort((a: any, b: any) => { const aScore = parseFloat(a.cutoff_score); const bScore = parseFloat(b.cutoff_score); return sortOrder === 'desc' ? bScore - aScore : aScore - bScore; }); } else if (sortBy === 'last_rank') { - mockData.sort((a, b) => { + mockData.sort((a: any, b: any) => { const aRank = parseInt(a.last_rank); const bRank = parseInt(b.last_rank); return sortOrder === 'desc' ? bRank - aRank : aRank - bRank; @@ -166,28 +161,19 @@ export async function GET(request: NextRequest) { filterParts.push(`(college_name ~ "${search}" || course_name ~ "${search}")`); } - if (categories) { - const categoryList = categories.split(',').filter(c => c.trim()); - if (categoryList.length > 0) { - const categoryFilter = categoryList.map(cat => `category = "${cat}"`).join(' || '); - filterParts.push(`(${categoryFilter})`); - } + if (categories && Array.isArray(categories) && categories.length > 0) { + const categoryFilter = categories.map((cat: string) => `category = "${cat}"`).join(' || '); + filterParts.push(`(${categoryFilter})`); } - if (courses) { - const courseList = courses.split(',').filter(c => c.trim()); - if (courseList.length > 0) { - const courseFilter = courseList.map(course => `course_name = "${course}"`).join(' || '); - filterParts.push(`(${courseFilter})`); - } + if (courses && Array.isArray(courses) && courses.length > 0) { + const courseFilter = courses.map((course: string) => `course_name = "${course}"`).join(' || '); + filterParts.push(`(${courseFilter})`); } - if (seatAllocations) { - const seatList = seatAllocations.split(',').filter(s => s.trim()); - if (seatList.length > 0) { - const seatFilter = seatList.map(seat => `seat_allocation_section = "${seat}"`).join(' || '); - filterParts.push(`(${seatFilter})`); - } + if (seatAllocations && Array.isArray(seatAllocations) && seatAllocations.length > 0) { + const seatFilter = seatAllocations.map((seat: string) => `seat_allocation_section = "${seat}"`).join(' || '); + filterParts.push(`(${seatFilter})`); } // Percentile-based filtering (-1 range only) - filtering cutoff_score directly @@ -326,7 +312,7 @@ export async function GET(request: NextRequest) { // Apply search filter to mock data if provided if (search) { - mockData = mockData.filter(item => + mockData = mockData.filter((item: any) => item.college_name.toLowerCase().includes(search.toLowerCase()) || item.course_name.toLowerCase().includes(search.toLowerCase()) ); @@ -339,23 +325,23 @@ export async function GET(request: NextRequest) { const minPercentile = Math.max(0, Math.round((targetPercentile - 1) * 10000000000) / 10000000000); const maxPercentile = Math.round(targetPercentile * 10000000000) / 10000000000; - mockData = mockData.filter(item => { + mockData = mockData.filter((item: any) => { const score = parseFloat(item.cutoff_score); return score >= minPercentile && score <= maxPercentile; }); // Sort by cutoff_score descending for percentile searches - mockData.sort((a, b) => parseFloat(b.cutoff_score) - parseFloat(a.cutoff_score)); + mockData.sort((a: any, b: any) => parseFloat(b.cutoff_score) - parseFloat(a.cutoff_score)); } else { // Apply other sorting when no percentile filter if (sortBy === 'cutoff_score') { - mockData.sort((a, b) => { + mockData.sort((a: any, b: any) => { const aScore = parseFloat(a.cutoff_score); const bScore = parseFloat(b.cutoff_score); return sortOrder === 'desc' ? bScore - aScore : aScore - bScore; }); } else if (sortBy === 'last_rank') { - mockData.sort((a, b) => { + mockData.sort((a: any, b: any) => { const aRank = parseInt(a.last_rank); const bRank = parseInt(b.last_rank); return sortOrder === 'desc' ? bRank - aRank : aRank - bRank; diff --git a/app/mht-cet/state-cutoffs/page.tsx b/app/mht-cet/state-cutoffs/page.tsx index ee0b6f7..1e10462 100644 --- a/app/mht-cet/state-cutoffs/page.tsx +++ b/app/mht-cet/state-cutoffs/page.tsx @@ -311,7 +311,7 @@ export default function StateCutoffsPage() { const [loading, setLoading] = useState(true); const [totalItems, setTotalItems] = useState(0); const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(10); + const [itemsPerPage, setItemsPerPage] = useState(25); const [isRequestActive, setIsRequestActive] = useState(false); // Applied filters (what's actually being used for search) @@ -365,10 +365,10 @@ export default function StateCutoffsPage() { ) }, @@ -376,17 +376,17 @@ export default function StateCutoffsPage() { -
+
{row.getValue("college_name")}
-

{row.getValue("college_name")}

+

{row.getValue("college_name")}

), - size: 250, + size: 300, }, { accessorKey: "course_name", @@ -395,10 +395,10 @@ export default function StateCutoffsPage() { ) }, @@ -406,17 +406,17 @@ export default function StateCutoffsPage() { -
+
{row.getValue("course_name")}
-

{row.getValue("course_name")}

+

{row.getValue("course_name")}

), - size: 200, + size: 250, }, { accessorKey: "category", @@ -425,19 +425,21 @@ export default function StateCutoffsPage() { ) }, cell: ({ row }) => ( - - {row.getValue("category")} - +
+ + {row.getValue("category")} + +
), - size: 100, + size: 120, }, { accessorKey: "last_rank", @@ -446,10 +448,10 @@ export default function StateCutoffsPage() { ) }, @@ -457,12 +459,12 @@ export default function StateCutoffsPage() { const rank = row.getValue("last_rank") as string; const numRank = parseInt(rank); return ( -
+
{isNaN(numRank) ? rank : numRank.toLocaleString()}
); }, - size: 80, + size: 100, }, { accessorKey: "cutoff_score", @@ -471,19 +473,19 @@ export default function StateCutoffsPage() { ) }, cell: ({ row }) => ( -
+
{row.getValue("cutoff_score")}
), - size: 80, + size: 100, }, { accessorKey: "seat_allocation_section", @@ -492,10 +494,10 @@ export default function StateCutoffsPage() { ) }, @@ -507,18 +509,18 @@ export default function StateCutoffsPage() { -
+
{label}
-

{label}

+

{label}

); }, - size: 120, + size: 150, }, { accessorKey: "total_admitted", @@ -527,10 +529,10 @@ export default function StateCutoffsPage() { ) }, @@ -540,18 +542,18 @@ export default function StateCutoffsPage() { -
+
{value.toLocaleString()}
-

Total students admitted: {value}

+

Total students admitted: {value}

); }, - size: 80, + size: 100, }, { accessorKey: "college_code", @@ -560,10 +562,10 @@ export default function StateCutoffsPage() { ) }, @@ -571,17 +573,17 @@ export default function StateCutoffsPage() { -
+
{row.getValue("college_code")}
-

College Code: {row.getValue("college_code")}

+

College Code: {row.getValue("college_code")}

), - size: 90, + size: 110, }, { accessorKey: "course_code", @@ -590,10 +592,10 @@ export default function StateCutoffsPage() { ) }, @@ -601,17 +603,17 @@ export default function StateCutoffsPage() { -
+
{row.getValue("course_code")}
-

Course Code: {row.getValue("course_code")}

+

Course Code: {row.getValue("course_code")}

), - size: 90, + size: 110, }, ], [] @@ -687,20 +689,19 @@ export default function StateCutoffsPage() { console.log('Fetching records for page:', currentPage, 'with filters:', debouncedFilters); - // Only cancel previous request if it's for different filters (not just pagination) - const params = new URLSearchParams({ - page: currentPage.toString(), - perPage: itemsPerPage.toString(), + const requestBody = { + page: currentPage, + perPage: itemsPerPage, search: debouncedFilters.search, - categories: debouncedFilters.categories.join(','), - seatAllocations: debouncedFilters.seatAllocations.join(','), - courses: debouncedFilters.courses.join(','), + categories: debouncedFilters.categories, + seatAllocations: debouncedFilters.seatAllocations, + courses: debouncedFilters.courses, percentileInput: debouncedFilters.percentileInput, sortBy: debouncedFilters.sortBy, - sortOrder: debouncedFilters.sortOrder - }); + sortOrder: debouncedFilters.sortOrder, + }; - const cacheKey = params.toString(); + const cacheKey = JSON.stringify(requestBody); // Check if this is the same request type (only different pagination) const currentRequestKey = `${debouncedFilters.search}-${debouncedFilters.categories.join(',')}-${debouncedFilters.seatAllocations.join(',')}-${debouncedFilters.courses.join(',')}-${debouncedFilters.percentileInput}`; @@ -735,13 +736,16 @@ export default function StateCutoffsPage() { setLoading(true); try { - console.log(`Making API request ${requestId} with params:`, params.toString()); + console.log(`Making API POST request ${requestId} with body:`, requestBody); - const response = await fetch(`/api/mht-cet/state-cutoffs?${params}`, { + const response = await fetch(`/api/mht-cet/state-cutoffs`, { + method: 'POST', signal: abortControllerRef.current.signal, headers: { + 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300', // Cache for 5 minutes - } + }, + body: JSON.stringify(requestBody), }); // Check if this is still the latest request @@ -924,39 +928,45 @@ export default function StateCutoffsPage() { }, [memoizedTotalItems, itemsPerPage]); return ( -
+
{/* Header */} -
-
-

+
+
+

MHT-CET State Cutoffs 2024

-

+

Round 1 cutoffs for engineering colleges

-
-
{/* Filters */} - - - + + + Filters {hasUnsavedChanges && ( - + Changes Pending )} - + {/* Search Input */}
@@ -964,14 +974,14 @@ export default function StateCutoffsPage() { placeholder="Search colleges, courses..." value={pendingFilters.search} onChange={(e) => setPendingFilters(prev => ({ ...prev, search: e.target.value }))} - className="pl-10 font-abel" + className="pl-10 font-abel text-sm md:text-base h-12" />
-
+
{/* Percentile Input */}
-
-
+
{/* Enhanced Category Filter */} - - +
-

Select Categories

+

Select Categories

@@ -1070,7 +1080,7 @@ export default function StateCutoffsPage() { checked={pendingFilters.categories.includes(category)} onCheckedChange={() => handleCategoryToggle(category)} /> -
@@ -1087,9 +1097,9 @@ export default function StateCutoffsPage() { {/* Enhanced Course Filter */} - - +
-

Select Courses

+

Select Courses

@@ -1145,13 +1155,14 @@ export default function StateCutoffsPage() {
{courses.map((course) => ( -
+
handleCourseToggle(course)} + className="mt-1" /> -
@@ -1168,9 +1179,9 @@ export default function StateCutoffsPage() { {/* Seat Allocation Filter */} - - -
- {SEAT_ALLOCATION_OPTIONS.map((option) => ( -
- handleSeatAllocationToggle(option.value)} - /> - -
- ))} -
+ + +
+ {SEAT_ALLOCATION_OPTIONS.map((option) => ( +
+ handleSeatAllocationToggle(option.value)} + className="mt-1" + /> + +
+ ))} +
+
{/* Search and Clear Buttons */} -
+
{/* Changes Indicator */} {!pendingFilters.percentileInput ? ( -
-
- +
+
+ Please enter a target percentile to search for cutoffs.
) : hasUnsavedChanges ? ( -
-
- +
+
+ You have unsaved filter changes. Click "Search Cutoffs" to apply them.
@@ -1249,68 +1266,68 @@ export default function StateCutoffsPage() { {/* Active Filters Display */} {(filters.percentileInput || filters.categories.length > 0 || filters.seatAllocations.length > 0 || filters.courses.length > 0) && ( - +
- -

Active Filters

+ +

Active Filters

{filters.percentileInput && ( - - Target Percentile: {filters.percentileInput}% + + Target: {filters.percentileInput}% )} {filters.categories.length > 0 && (
- - Categories ({filters.categories.length}): + + Categories ({filters.categories.length}) - {filters.categories.slice(0, 3).map((category, index) => ( - + {filters.categories.slice(0, 2).map((category, index) => ( + {category} ))} - {filters.categories.length > 3 && ( - - +{filters.categories.length - 3} more + {filters.categories.length > 2 && ( + + +{filters.categories.length - 2} more )}
)} {filters.seatAllocations.length > 0 && (
- - Seat Allocations ({filters.seatAllocations.length}): + + Seats ({filters.seatAllocations.length}) - {filters.seatAllocations.slice(0, 2).map((allocation, index) => { + {filters.seatAllocations.slice(0, 1).map((allocation, index) => { const option = SEAT_ALLOCATION_OPTIONS.find(s => s.value === allocation); const label = option?.label || allocation; return ( - + {label} ); })} - {filters.seatAllocations.length > 2 && ( - - +{filters.seatAllocations.length - 2} more + {filters.seatAllocations.length > 1 && ( + + +{filters.seatAllocations.length - 1} more )}
)} {filters.courses.length > 0 && (
- - Courses ({filters.courses.length}): + + Courses ({filters.courses.length}) - {filters.courses.slice(0, 2).map((course, index) => ( - - {course.length > 30 ? `${course.substring(0, 30)}...` : course} + {filters.courses.slice(0, 1).map((course, index) => ( + + {course.length > 20 ? `${course.substring(0, 20)}...` : course} ))} - {filters.courses.length > 2 && ( - - +{filters.courses.length - 2} more + {filters.courses.length > 1 && ( + + +{filters.courses.length - 1} more )}
@@ -1321,32 +1338,32 @@ export default function StateCutoffsPage() { )} {/* Compact Results Summary */} -
-
+
+
{memoizedTotalItems.toLocaleString()} total
-
+
{records.length} showing
-
+
{filters.categories.length} categories
-
+
{filters.percentileInput ? '-1%' : 'All'} - percentile range + range
{/* Pagination Controls */} -
-

+

+

Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, memoizedTotalItems)} of {memoizedTotalItems.toLocaleString()} results

-
- Rows per page: +
+ Rows per page: { @@ -1485,7 +1504,7 @@ export default function StateCutoffsPage() { setCurrentPage(1); // Reset to first page when changing items per page }} > - + @@ -1498,13 +1517,14 @@ export default function StateCutoffsPage() {
-
- Page {currentPage} of {Math.ceil(memoizedTotalItems / itemsPerPage)} -
-
+
+
+ Page {currentPage} of {Math.ceil(memoizedTotalItems / itemsPerPage)} +
+
{/* Table Info */} -
-
+
+
Showing {table.getRowModel().rows.length} of {memoizedTotalItems.toLocaleString()} results
-
+
{table.getFilteredRowModel().rows.length} row(s) displayed.
@@ -1563,42 +1583,42 @@ export default function StateCutoffsPage() { {/* Informational Cards */} -
+
{/* How to Use This Tool */} - -
- + +
+
How to Use This Tool
- -
-
-
1
+ +
+
+
1

Enter Your Percentile

Type your MHT-CET percentile to see colleges in a -1% range.

-
-
2
+
+
2

Filter Your Preferences

Select categories, courses, and seat types to narrow down results.

-
-
3
+
+
3

Analyze & Strategize

Results are sorted by highest cutoff. Use this to plan your CAP round choices.

-
+

Pro Tip: The -1% range shows realistic options, helping you find colleges where you have a strong chance of admission.

@@ -1609,30 +1629,30 @@ export default function StateCutoffsPage() { {/* Seat Allocation Types */} - -
- + +
+
Seat Allocation Types
- -
- + +
+

State Level

Open to all Maharashtra candidates.

-
- +
+

Home University

For students within the same university region.

-
- +
+

Other University

For students from different university regions.

@@ -1642,39 +1662,39 @@ export default function StateCutoffsPage() { {/* Category and Code Legends */} - + - -
- + +
+
Category & Code Legends
- - + +
- Category Code Format + Category Code Format
-
+

G = General, L = Ladies

H = Home Uni, O = Other Uni, S = State

-

Example: GOPENH is General Open Home University.

+

Example: GOPENH is General Open Home University.

- - + +
- Special Category Codes + Special Category Codes
-
+

TFWS: Tuition Fee Waiver Scheme

EWS: Economically Weaker Section

DEF: Defence Reserved

@@ -1683,10 +1703,10 @@ export default function StateCutoffsPage() {
- - + +
-
@@ -1703,7 +1723,7 @@ export default function StateCutoffsPage() { allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen - style={{ minHeight: '200px' }} + style={{ minHeight: '150px' }} >
diff --git a/components/footer.tsx b/components/footer.tsx index fc2e3a2..f45d0f3 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -125,7 +125,7 @@ export default function Footer() {

- Meow 👋! I'm Kartik, the creator. You can follow me on Reddit or Twitter! + Meow 👋! I'm Kartik, the creator. You can follow me on Reddit or Twitter!

diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx index b2b5c71..c4ebaf3 100644 --- a/components/ui/accordion.tsx +++ b/components/ui/accordion.tsx @@ -1,62 +1,72 @@ -'use client' - -import * as AccordionPrimitive from '@radix-ui/react-accordion' -import { ChevronDown } from 'lucide-react' - -import * as React from 'react' - -import { cn } from '@/lib/utils' - -const Accordion = AccordionPrimitive.Root - -const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AccordionItem.displayName = 'AccordionItem' - -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - ) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + svg]:rotate-180 [&[data-state=open]]:rounded-b-none [&[data-state=open]]:border-b-2', + "rounded-base overflow-hidden border-2 border-b border-border shadow-shadow", className, )} {...props} + /> + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180 data-[state=open]:rounded-b-none data-[state=open]:border-b-2 disabled:pointer-events-none disabled:opacity-50", + className, + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + - {children} - - - -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)) +
{children}
+ + ) +} AccordionContent.displayName = AccordionPrimitive.Content.displayName -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } \ No newline at end of file +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/lib/pocketbaseClient.ts b/lib/pocketbaseClient.ts index 725d062..7652df8 100644 --- a/lib/pocketbaseClient.ts +++ b/lib/pocketbaseClient.ts @@ -1,5 +1,4 @@ -import PocketBase from 'pocketbase'; -import { ClientResponseError } from 'pocketbase'; +import PocketBase, { ClientResponseError } from 'pocketbase'; /** * Global auth cache to persist across API calls @@ -12,23 +11,24 @@ let globalAuthCache: { const AUTH_CACHE_DURATION = 50 * 60 * 1000; // 50 minutes +// Added cache for the PocketBase client instance +let pbClient: PocketBase | null = null; + /** * Get PocketBase instance (singleton pattern) */ -let pb: PocketBase | null = null; - export function getPocketBase() { - if (!pb) { + if (!pbClient) { const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL; if (!pocketbaseUrl) { throw new Error('NEXT_PUBLIC_POCKETBASE_URL environment variable is not defined'); } - pb = new PocketBase(pocketbaseUrl); - pb.autoCancellation(false); + pbClient = new PocketBase(pocketbaseUrl); + pbClient.autoCancellation(false); } - return pb; + return pbClient; } /** diff --git a/tailwind.config.ts b/tailwind.config.ts index 6d400a3..d7d9091 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -20,6 +20,7 @@ const config = { }, extend: { screens: { + 'xs': '480px', m1500: { raw: '(max-width: 1500px)' }, m1300: { raw: '(max-width: 1300px)' }, m1100: { raw: '(max-width: 1100px)' },