From 5da17de13426d10b1580246ae0925bd55f973057 Mon Sep 17 00:00:00 2001 From: advikgupta Date: Sat, 7 Jun 2025 17:13:58 +0530 Subject: [PATCH 1/7] created users paper section --- src/app/api/upcoming-papers/route.ts | 9 +- src/app/api/user-papers/route.ts | 42 ++++++++++ src/components/UsersPapers.tsx | 120 +++++++++++++++++++++++++++ src/components/screens/Hero.tsx | 4 +- 4 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/app/api/user-papers/route.ts create mode 100644 src/components/UsersPapers.tsx diff --git a/src/app/api/upcoming-papers/route.ts b/src/app/api/upcoming-papers/route.ts index 83f78b0..8b426fa 100644 --- a/src/app/api/upcoming-papers/route.ts +++ b/src/app/api/upcoming-papers/route.ts @@ -18,8 +18,13 @@ export async function GET() { { status: 404 }, ); } - const nextSlot = String.fromCharCode(slot.charCodeAt(0) + 1) - const correspondingSlots = [slot + "1", slot + "2", nextSlot + "1", nextSlot + "2"]; + const nextSlot = String.fromCharCode(slot.charCodeAt(0) + 1); + const correspondingSlots = [ + slot + "1", + slot + "2", + nextSlot + "1", + nextSlot + "2", + ]; const selectedSubjects = await UpcomingSubject.find({ slots: { $in: correspondingSlots }, }); diff --git a/src/app/api/user-papers/route.ts b/src/app/api/user-papers/route.ts new file mode 100644 index 0000000..5b552c2 --- /dev/null +++ b/src/app/api/user-papers/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import { connectToDatabase } from "@/lib/mongoose"; +import Paper from "@/db/papers"; + +export const dynamic = "force-dynamic"; + +export async function POST(req: Request) { + try { + await connectToDatabase(); + const body = await req.json(); + + const subjects: string[] = body; + + const usersPapers = await Paper.find({ + subject: { $in: subjects }, + }); + + let transformedPapers = usersPapers.map((paper) => ({ + subject: paper.subject, + slots: [paper.slot], + })); + + // check duplicates + transformedPapers = Array.from( + new Map(transformedPapers.map((item) => [item.subject, item])).values(), + ); + + console.log("usersPapers", usersPapers); + + return NextResponse.json(transformedPapers, { + status: 200, + }); + } catch (error) { + console.error("Error fetching papers:", error); + return NextResponse.json( + { + error: "Failed to fetch papers.", + }, + { status: 500 }, + ); + } +} diff --git a/src/components/UsersPapers.tsx b/src/components/UsersPapers.tsx new file mode 100644 index 0000000..1a10e33 --- /dev/null +++ b/src/components/UsersPapers.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useEffect, useState } from "react"; +import axios from "axios"; +import { type IUpcomingPaper } from "@/interface"; +import Loader from "./ui/loader"; +import UpcomingPaper from "./UpcomingPaper"; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "@/components/ui/carousel"; +import Autoplay from "embla-carousel-autoplay"; +import { chunkArray } from "@/util/utils"; + +function UsersPapers() { + const [displayPapers, setDisplayPapers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [chunkSize, setChunkSize] = useState(4); + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 640) { + setChunkSize(4); + } else { + setChunkSize(8); + } + }; + + localStorage.setItem( + "userSubjects", + JSON.stringify([ + "Information Security [CBS3002]", + "Foundations of Data Analytics [BCSE351E]", + "Design and Analysis of Algorithms [MCSE502L]", + "Complex Variables and Linear Algebra [BMAT201L]", + "Differential Equations and Transforms [BMAT102L]", + ]), + ); + + handleResize(); + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + const chunkedPapers = chunkArray(displayPapers, chunkSize); + + useEffect(() => { + async function fetchPapers() { + try { + const storedSubjects = JSON.parse(localStorage.getItem("userSubjects")); + setIsLoading(true); + const response = await axios.post("/api/user-papers", storedSubjects); + setDisplayPapers(response.data); + } catch (error) { + console.error("Failed to fetch papers:", error); + } finally { + setIsLoading(false); + } + } + + void fetchPapers(); + }, []); + + if (isLoading) { + return ; + } + + const plugins = [Autoplay({ delay: 8000, stopOnInteraction: true })]; + + return ( +
+

+ Users Papers +

+ +
+ +
+ + +
+ + {chunkedPapers.map((paperGroup, index) => { + return ( + + {paperGroup.map((paper, subIndex) => ( +
+ +
+ ))} +
+ ); + })} +
+
+
+
+ ); +} + +export default UsersPapers; diff --git a/src/components/screens/Hero.tsx b/src/components/screens/Hero.tsx index 3cb0ffc..822a9c7 100644 --- a/src/components/screens/Hero.tsx +++ b/src/components/screens/Hero.tsx @@ -1,16 +1,18 @@ import React from "react"; import SearchBar from "../Searchbar/searchbar"; import StoredPapers from "../StoredPapers"; +import UsersPapers from "../UsersPapers"; const Hero = () => { return (
-

+

Built by Students for Students

+ {/*

Learn More

From 6fb6e87f30f4ea97705a338bf22c75c7c62e17f4 Mon Sep 17 00:00:00 2001 From: advikgupta Date: Sat, 7 Jun 2025 23:03:27 +0530 Subject: [PATCH 2/7] created PapersCarousel template component --- src/app/api/papers/route.ts | 2 +- src/app/api/user-papers/route.ts | 28 +++-- .../{UsersPapers.tsx => PapersCarousel.tsx} | 25 +++- src/components/Searchbar/searchbar-child.tsx | 23 +++- src/components/StoredPapers.tsx | 110 ------------------ src/components/screens/Hero.tsx | 7 +- 6 files changed, 61 insertions(+), 134 deletions(-) rename src/components/{UsersPapers.tsx => PapersCarousel.tsx} (82%) delete mode 100644 src/components/StoredPapers.tsx diff --git a/src/app/api/papers/route.ts b/src/app/api/papers/route.ts index 12186d0..5a33b13 100644 --- a/src/app/api/papers/route.ts +++ b/src/app/api/papers/route.ts @@ -29,7 +29,7 @@ export async function GET(req: NextRequest) { if (papers.length === 0) { return NextResponse.json( { message: "No papers found for the specified subject" }, - { status: 404 }, + { status: 200 }, ); } diff --git a/src/app/api/user-papers/route.ts b/src/app/api/user-papers/route.ts index 5b552c2..7cef1f4 100644 --- a/src/app/api/user-papers/route.ts +++ b/src/app/api/user-papers/route.ts @@ -15,19 +15,27 @@ export async function POST(req: Request) { subject: { $in: subjects }, }); - let transformedPapers = usersPapers.map((paper) => ({ - subject: paper.subject, - slots: [paper.slot], - })); + const transformedPapers = usersPapers.reduce((acc, paper) => { + const existing = acc.find((item) => item.subject === paper.subject); - // check duplicates - transformedPapers = Array.from( - new Map(transformedPapers.map((item) => [item.subject, item])).values(), - ); + if (existing) { + existing.slots.push(paper.slot); + } else { + acc.push({ subject: paper.subject, slots: [paper.slot] }); + } + + return acc; + }, []); - console.log("usersPapers", usersPapers); + // check duplicates + const seenSubjects = new Set(); + const uniquePapers = transformedPapers.filter((paper) => { + if (seenSubjects.has(paper.subject)) return false; + seenSubjects.add(paper.subject); + return true; + }); - return NextResponse.json(transformedPapers, { + return NextResponse.json(uniquePapers, { status: 200, }); } catch (error) { diff --git a/src/components/UsersPapers.tsx b/src/components/PapersCarousel.tsx similarity index 82% rename from src/components/UsersPapers.tsx rename to src/components/PapersCarousel.tsx index 1a10e33..287e6a6 100644 --- a/src/components/UsersPapers.tsx +++ b/src/components/PapersCarousel.tsx @@ -15,7 +15,11 @@ import { import Autoplay from "embla-carousel-autoplay"; import { chunkArray } from "@/util/utils"; -function UsersPapers() { +function PapersCarousel({ + carouselType, +}: { + carouselType: "users" | "default"; +}) { const [displayPapers, setDisplayPapers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [chunkSize, setChunkSize] = useState(4); @@ -53,10 +57,19 @@ function UsersPapers() { useEffect(() => { async function fetchPapers() { try { - const storedSubjects = JSON.parse(localStorage.getItem("userSubjects")); setIsLoading(true); - const response = await axios.post("/api/user-papers", storedSubjects); - setDisplayPapers(response.data); + if (carouselType === "users") { + const storedSubjects = JSON.parse( + localStorage.getItem("userSubjects"), + ); + const response = await axios.post("/api/user-papers", storedSubjects); + setDisplayPapers(response.data); + } else { + const response = await axios.get( + "/api/upcoming-papers", + ); + setDisplayPapers(response.data); + } } catch (error) { console.error("Failed to fetch papers:", error); } finally { @@ -76,7 +89,7 @@ function UsersPapers() { return (

- Users Papers + {carouselType === "users" ? "Your Papers" : "Upcoming Papers"}

@@ -117,4 +130,4 @@ function UsersPapers() { ); } -export default UsersPapers; +export default PapersCarousel; diff --git a/src/components/Searchbar/searchbar-child.tsx b/src/components/Searchbar/searchbar-child.tsx index 4359731..3e7ba10 100644 --- a/src/components/Searchbar/searchbar-child.tsx +++ b/src/components/Searchbar/searchbar-child.tsx @@ -25,9 +25,17 @@ function SearchBarChild({ const response = await axios.get("/api/papers", { params: { subject: subjectName }, }); - return response.data.papers.length; // Assuming the API returns an array of papers + + if ( + response.data.message === "No papers found for the specified subject" + ) { + return 0; + } + + return response.data.papers.length; } catch (error) { - return 0; // Return 0 if there's no papers found or an error occurs + console.error("Error fetching paper quantity:", error); + return "request-error"; } }; @@ -44,7 +52,16 @@ function SearchBarChild({ .map((item) => item.item) .slice(0, 10); - setSuggestions(filteredSuggestions); + const suggestionsWithCount = await Promise.all( + filteredSuggestions.map(async (suggestion) => { + const count = await fetchPaperQuantityByName(suggestion); + return count !== "request-error" + ? `${suggestion} (${count})` + : suggestion; + }), + ); + + setSuggestions(suggestionsWithCount); } else { setSuggestions([]); } diff --git a/src/components/StoredPapers.tsx b/src/components/StoredPapers.tsx deleted file mode 100644 index 5e84ce3..0000000 --- a/src/components/StoredPapers.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import axios from "axios"; -import { type IUpcomingPaper } from "@/interface"; -import Loader from "./ui/loader"; -import UpcomingPaper from "./UpcomingPaper"; -import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, -} from "@/components/ui/carousel"; -import Autoplay from "embla-carousel-autoplay"; -import { chunkArray } from "@/util/utils"; - -function StoredPapers() { - const [displayPapers, setDisplayPapers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [chunkSize, setChunkSize] = useState(4); - - useEffect(() => { - const handleResize = () => { - if (window.innerWidth < 640) { - setChunkSize(4); - } else { - setChunkSize(8); - } - }; - - handleResize(); - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - const chunkedPapers = chunkArray(displayPapers, chunkSize); - - useEffect(() => { - async function fetchPapers() { - try { - setIsLoading(true); - const response = await axios.get( - "/api/upcoming-papers", - ); - setDisplayPapers(response.data); - } catch (error) { - console.error("Failed to fetch papers:", error); - } finally { - setIsLoading(false); - } - } - - void fetchPapers(); - }, []); - - if (isLoading) { - return ; - } - - const plugins = [Autoplay({ delay: 8000, stopOnInteraction: true })]; - - return ( -
-

- Upcoming Papers -

- -
- -
- - -
- - {chunkedPapers.map((paperGroup, index) => { - return ( - - {paperGroup.map((paper, subIndex) => ( -
- -
- ))} -
- ); - })} -
-
-
-
- ); -} - -export default StoredPapers; diff --git a/src/components/screens/Hero.tsx b/src/components/screens/Hero.tsx index 822a9c7..b85e9dd 100644 --- a/src/components/screens/Hero.tsx +++ b/src/components/screens/Hero.tsx @@ -1,7 +1,6 @@ import React from "react"; import SearchBar from "../Searchbar/searchbar"; -import StoredPapers from "../StoredPapers"; -import UsersPapers from "../UsersPapers"; +import PapersCarousel from "../PapersCarousel"; const Hero = () => { return ( @@ -12,8 +11,8 @@ const Hero = () => {
- - + + {/*

Learn More

Date: Fri, 13 Jun 2025 00:49:47 +0530 Subject: [PATCH 3/7] made paper count route in api --- src/app/api/papers/count/route.ts | 15 +++++----- src/components/Searchbar/searchbar-child.tsx | 30 +------------------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/app/api/papers/count/route.ts b/src/app/api/papers/count/route.ts index 48f3bbc..1dba8e4 100644 --- a/src/app/api/papers/count/route.ts +++ b/src/app/api/papers/count/route.ts @@ -4,20 +4,21 @@ import Paper from "@/db/papers"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(req: Request) { try { await connectToDatabase(); - const count: number = await Paper.countDocuments(); + const { searchParams } = new URL(req.url); + const subject = searchParams.get("subject"); - return NextResponse.json( - { count }, - { status: 200 } - ); + const filter = subject ? { subject } : {}; + const count = await Paper.countDocuments(filter); + + return NextResponse.json({ count }, { status: 200 }); } catch (error) { return NextResponse.json( { message: "Failed to fetch papers", error }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/components/Searchbar/searchbar-child.tsx b/src/components/Searchbar/searchbar-child.tsx index 3e7ba10..7d44e17 100644 --- a/src/components/Searchbar/searchbar-child.tsx +++ b/src/components/Searchbar/searchbar-child.tsx @@ -20,25 +20,6 @@ function SearchBarChild({ const suggestionsRef = useRef(null); const fuzzy = new Fuse(initialSubjects); - const fetchPaperQuantityByName = async (subjectName: string) => { - try { - const response = await axios.get("/api/papers", { - params: { subject: subjectName }, - }); - - if ( - response.data.message === "No papers found for the specified subject" - ) { - return 0; - } - - return response.data.papers.length; - } catch (error) { - console.error("Error fetching paper quantity:", error); - return "request-error"; - } - }; - const handleSearchChange = async (e: React.ChangeEvent) => { const text = e.target.value; setSearchText(text); @@ -52,16 +33,7 @@ function SearchBarChild({ .map((item) => item.item) .slice(0, 10); - const suggestionsWithCount = await Promise.all( - filteredSuggestions.map(async (suggestion) => { - const count = await fetchPaperQuantityByName(suggestion); - return count !== "request-error" - ? `${suggestion} (${count})` - : suggestion; - }), - ); - - setSuggestions(suggestionsWithCount); + setSuggestions(filteredSuggestions); } else { setSuggestions([]); } From f1ad13050c6eaab84cee4ac51e87122bc3d9aed5 Mon Sep 17 00:00:00 2001 From: Advik-Gupta Date: Wed, 18 Jun 2025 14:14:46 +0530 Subject: [PATCH 4/7] made carousel type default to upcoming --- src/components/PapersCarousel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PapersCarousel.tsx b/src/components/PapersCarousel.tsx index 287e6a6..a00e968 100644 --- a/src/components/PapersCarousel.tsx +++ b/src/components/PapersCarousel.tsx @@ -16,9 +16,9 @@ import Autoplay from "embla-carousel-autoplay"; import { chunkArray } from "@/util/utils"; function PapersCarousel({ - carouselType, + carouselType = "upcoming", }: { - carouselType: "users" | "default"; + carouselType: "users" | "upcoming"; }) { const [displayPapers, setDisplayPapers] = useState([]); const [isLoading, setIsLoading] = useState(true); From a433e41d56f396747ead49629d6a8638c80709f2 Mon Sep 17 00:00:00 2001 From: Advik-Gupta Date: Fri, 18 Jul 2025 23:53:41 +0530 Subject: [PATCH 5/7] created pinned papers page --- src/app/api/user-papers/route.ts | 27 +-- src/app/pinned/page.tsx | 25 +++ src/components/AddPapers.tsx | 14 ++ src/components/PapersCarousel.tsx | 35 +--- src/components/PinButton.tsx | 27 +++ src/components/PinnedPapersCarousel.tsx | 156 ++++++++++++++++++ src/components/Searchbar/pinned-searchbar.tsx | 141 ++++++++++++++++ src/components/Searchbar/searchbar.tsx | 9 +- src/components/UpcomingPaper.tsx | 54 ++++-- src/components/screens/Hero.tsx | 3 +- src/interface.ts | 48 ++++-- 11 files changed, 466 insertions(+), 73 deletions(-) create mode 100644 src/app/pinned/page.tsx create mode 100644 src/components/AddPapers.tsx create mode 100644 src/components/PinButton.tsx create mode 100644 src/components/PinnedPapersCarousel.tsx create mode 100644 src/components/Searchbar/pinned-searchbar.tsx diff --git a/src/app/api/user-papers/route.ts b/src/app/api/user-papers/route.ts index 7cef1f4..ae1c699 100644 --- a/src/app/api/user-papers/route.ts +++ b/src/app/api/user-papers/route.ts @@ -1,33 +1,36 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; import Paper from "@/db/papers"; +import { StoredSubjects } from "@/interface"; export const dynamic = "force-dynamic"; export async function POST(req: Request) { try { await connectToDatabase(); - const body = await req.json(); + const body = (await req.json()) as StoredSubjects; - const subjects: string[] = body; + const subjects = body; const usersPapers = await Paper.find({ subject: { $in: subjects }, }); - const transformedPapers = usersPapers.reduce((acc, paper) => { - const existing = acc.find((item) => item.subject === paper.subject); + const transformedPapers = usersPapers.reduce( + (acc: { subject: string; slots: string[] }[], paper) => { + const existing = acc.find((item) => item.subject === paper.subject); - if (existing) { - existing.slots.push(paper.slot); - } else { - acc.push({ subject: paper.subject, slots: [paper.slot] }); - } + if (existing) { + existing.slots.push(paper.slot); + } else { + acc.push({ subject: paper.subject, slots: [paper.slot] }); + } - return acc; - }, []); + return acc; + }, + [], + ); - // check duplicates const seenSubjects = new Set(); const uniquePapers = transformedPapers.filter((paper) => { if (seenSubjects.has(paper.subject)) return false; diff --git a/src/app/pinned/page.tsx b/src/app/pinned/page.tsx new file mode 100644 index 0000000..ff827e1 --- /dev/null +++ b/src/app/pinned/page.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import SearchBar from "@/components/Searchbar/searchbar"; +import PinnedPapersCarousel from "@/components/PinnedPapersCarousel"; + +const Pinned = () => { + return ( +
+

+ Pinned Papers +

+ +
+
+ +
+
+ +
+

You can pin upto 8 Subjects

+
+
+ ); +}; + +export default Pinned; diff --git a/src/components/AddPapers.tsx b/src/components/AddPapers.tsx new file mode 100644 index 0000000..6acb6d6 --- /dev/null +++ b/src/components/AddPapers.tsx @@ -0,0 +1,14 @@ +const AddPapers = ({ onClick }: { onClick?: () => void }) => { + return ( +
+ + + + +
+ ); +}; + +export default AddPapers; diff --git a/src/components/PapersCarousel.tsx b/src/components/PapersCarousel.tsx index a00e968..4c9d8d8 100644 --- a/src/components/PapersCarousel.tsx +++ b/src/components/PapersCarousel.tsx @@ -15,11 +15,7 @@ import { import Autoplay from "embla-carousel-autoplay"; import { chunkArray } from "@/util/utils"; -function PapersCarousel({ - carouselType = "upcoming", -}: { - carouselType: "users" | "upcoming"; -}) { +function PapersCarousel() { const [displayPapers, setDisplayPapers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [chunkSize, setChunkSize] = useState(4); @@ -33,17 +29,6 @@ function PapersCarousel({ } }; - localStorage.setItem( - "userSubjects", - JSON.stringify([ - "Information Security [CBS3002]", - "Foundations of Data Analytics [BCSE351E]", - "Design and Analysis of Algorithms [MCSE502L]", - "Complex Variables and Linear Algebra [BMAT201L]", - "Differential Equations and Transforms [BMAT102L]", - ]), - ); - handleResize(); window.addEventListener("resize", handleResize); @@ -58,18 +43,10 @@ function PapersCarousel({ async function fetchPapers() { try { setIsLoading(true); - if (carouselType === "users") { - const storedSubjects = JSON.parse( - localStorage.getItem("userSubjects"), - ); - const response = await axios.post("/api/user-papers", storedSubjects); - setDisplayPapers(response.data); - } else { - const response = await axios.get( - "/api/upcoming-papers", - ); - setDisplayPapers(response.data); - } + const response = await axios.get( + "/api/upcoming-papers", + ); + setDisplayPapers(response.data); } catch (error) { console.error("Failed to fetch papers:", error); } finally { @@ -89,7 +66,7 @@ function PapersCarousel({ return (

- {carouselType === "users" ? "Your Papers" : "Upcoming Papers"} + Upcoming Papers

diff --git a/src/components/PinButton.tsx b/src/components/PinButton.tsx new file mode 100644 index 0000000..fc71593 --- /dev/null +++ b/src/components/PinButton.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Star, StarOff } from "lucide-react"; + +export default function PinButton({ + isPinned, + onToggle, +}: { + isPinned: boolean; + onToggle?: () => void; +}) { + return ( + + ); +} diff --git a/src/components/PinnedPapersCarousel.tsx b/src/components/PinnedPapersCarousel.tsx new file mode 100644 index 0000000..bf3aef8 --- /dev/null +++ b/src/components/PinnedPapersCarousel.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useEffect, useState } from "react"; +import axios from "axios"; +import { type IUpcomingPaper } from "@/interface"; +import Loader from "./ui/loader"; +import UpcomingPaper from "./UpcomingPaper"; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "@/components/ui/carousel"; +import AddPapers from "./AddPapers"; +import Autoplay from "embla-carousel-autoplay"; +import { chunkArray } from "@/util/utils"; +import { StoredSubjects } from "@/interface"; + +function PinnedPapersCarousel({ + carouselType = "upcoming", +}: { + carouselType: "users" | "upcoming"; +}) { + const [displayPapers, setDisplayPapers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [chunkSize, setChunkSize] = useState(4); + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 640) { + setChunkSize(4); + } else { + setChunkSize(8); + } + }; + + handleResize(); + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + const chunkedPapers = chunkArray(displayPapers, chunkSize); + + const fetchPapers = async () => { + try { + setIsLoading(true); + + const storedSubjects = JSON.parse( + localStorage.getItem("userSubjects") ?? "[]", + ) as StoredSubjects; + + const response = await axios.post<{ subject: string; slots: string[] }[]>( + "/api/user-papers", + storedSubjects, + ); + const fetchedPapers = response.data; + + const fetchedSubjectsSet = new Set( + fetchedPapers.map((paper) => paper.subject), + ); + + const storedSubjectsArray = Array.isArray(storedSubjects) + ? storedSubjects + : []; + const missingSubjects = storedSubjectsArray + .filter((subject: string) => !fetchedSubjectsSet.has(subject)) + .map((subject: string) => ({ + subject, + slots: [], + })) as { subject: string; slots: string[] }[]; + + const allDisplayPapers = [...fetchedPapers, ...missingSubjects]; + + setDisplayPapers(allDisplayPapers); + } catch (error) { + console.error("Failed to fetch papers:", error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + void fetchPapers(); + }, []); + + useEffect(() => { + const handleSubjectsChange = () => { + void fetchPapers(); + }; + + window.addEventListener("userSubjectsChanged", handleSubjectsChange); + + return () => { + window.removeEventListener("userSubjectsChanged", handleSubjectsChange); + }; + }, []); + + if (isLoading) { + return ; + } + + const plugins = [Autoplay({ delay: 8000, stopOnInteraction: true })]; + + return ( +
+
+ +
+ + +
+ + {chunkedPapers.map((paperGroup, index) => { + const isLastChunk = index === chunkedPapers.length - 1; + + return ( + + {paperGroup.map((paper, subIndex) => ( +
+ +
+ ))} + + {isLastChunk && displayPapers.length < 8 && ( +
+ +
+ )} +
+ ); + })} +
+
+
+
+ ); +} + +export default PinnedPapersCarousel; diff --git a/src/components/Searchbar/pinned-searchbar.tsx b/src/components/Searchbar/pinned-searchbar.tsx new file mode 100644 index 0000000..032020c --- /dev/null +++ b/src/components/Searchbar/pinned-searchbar.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { Search } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { Input } from "@/components/ui/input"; +import PinButton from "../PinButton"; +import Fuse from "fuse.js"; +import axios from "axios"; +import { set } from "mongoose"; +import { StoredSubjects } from "@/interface"; + +function PinnedSearchBar({ + initialSubjects, + filtersNotPulled, +}: { + initialSubjects: string[]; + filtersNotPulled?: () => void; +}) { + const router = useRouter(); + const [searchText, setSearchText] = useState(""); + const [suggestions, setSuggestions] = useState([]); + const suggestionsRef = useRef(null); + const [pinned, setPinned] = useState(false); + const fuzzy = new Fuse(initialSubjects); + + const handleSearchChange = async (e: React.ChangeEvent) => { + const text = e.target.value; + setSearchText(text); + + if (text.length > 1 && initialSubjects.length > 0) { + const filteredSuggestions = fuzzy + .search(text) + .sort((a, b) => (a.score ?? Infinity) - (b.score ?? Infinity)) + .map((item) => item.item) + .slice(0, 10); + + setSuggestions(filteredSuggestions); + } else { + setSuggestions([]); + } + }; + + const handleSelectSuggestion = (suggestion: string) => { + setSearchText(suggestion); + + const currentPinnedSubjects = JSON.parse( + localStorage.getItem("userSubjects") ?? "[]", + ) as StoredSubjects; + + if (currentPinnedSubjects.subjects.includes(suggestion)) { + setPinned(true); + } else { + setPinned(false); + } + + setSuggestions([]); + filtersNotPulled?.(); + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + suggestionsRef.current && + !suggestionsRef.current.contains(event.target as Node) + ) { + setSuggestions([]); + } + }; + + const hanglePinToggle = () => { + const current = !pinned; + setPinned(current); + const saved = JSON.parse( + localStorage.getItem("userSubjects") ?? "[]", + ) as StoredSubjects; + const updated = current + ? [...new Set([...saved.subjects, searchText])] + : saved.subjects.filter((s: string) => s !== searchText); + localStorage.setItem("userSubjects", JSON.stringify(updated)); + window.dispatchEvent(new Event("userSubjectsChanged")); + }; + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + return ( +
+
{ + e.preventDefault(); + }} + > +
+
+ 0 ? "rounded-b-none" : "" + }`} + /> + + {suggestions.length > 0 && ( +
    6 ? "h-[250px]" : "h-auto" + } ${suggestions.length > 10 ? "md:h-[400px]" : "md:h-auto"}`} + > + {suggestions.map((suggestion, index) => ( +
  • handleSelectSuggestion(suggestion)} + className="cursor-pointer truncate p-2 hover:bg-gray-100 dark:hover:bg-gray-800" + > + {suggestion} +
  • + ))} +
+ )} +
+ +
+
+
+ ); +} + +export default PinnedSearchBar; diff --git a/src/components/Searchbar/searchbar.tsx b/src/components/Searchbar/searchbar.tsx index c43fd33..83f004d 100644 --- a/src/components/Searchbar/searchbar.tsx +++ b/src/components/Searchbar/searchbar.tsx @@ -3,6 +3,7 @@ import axios from "axios"; import { type ICourses } from "@/interface"; import SearchBarChild from "./searchbar-child"; +import PinnedSearchBar from "./pinned-searchbar"; export async function fetchSubjects() { try { @@ -16,8 +17,12 @@ export async function fetchSubjects() { } } -export default async function SearchBar() { +export default async function SearchBar({ type = "default" }) { const subjects = await fetchSubjects(); - return ; + return type === "pinned" ? ( + + ) : ( + + ); } diff --git a/src/components/UpcomingPaper.tsx b/src/components/UpcomingPaper.tsx index 4b8fc22..8a333b8 100644 --- a/src/components/UpcomingPaper.tsx +++ b/src/components/UpcomingPaper.tsx @@ -4,7 +4,8 @@ import { extractWithoutBracketContent, } from "@/util/utils"; import { useRouter } from "next/navigation"; -import React from "react"; +import React, { useEffect, useState } from "react"; +import axios from "axios"; interface PaperCardProps { subject: string; @@ -14,33 +15,62 @@ interface PaperCardProps { export default function PaperCard({ subject, slots }: PaperCardProps) { const courseName = extractWithoutBracketContent(subject); const courseCode = extractBracketContent(subject); + const [paperCount, setPaperCount] = useState(0); + + useEffect(() => { + const fetchPaperCount = async () => { + try { + const response = await axios.get<{ count: number }>( + "/api/papers/count", + { + params: { subject }, + }, + ); + setPaperCount(response.data.count); + } catch (error) { + console.error("Failed to fetch paper count:", error); + } + }; + + void fetchPaperCount(); + }, [subject]); const router = useRouter(); return (
{ + if (!paperCount) return; // disable click if no papers e.preventDefault(); - const queryParams = new URLSearchParams({ - subject, - }); - + const queryParams = new URLSearchParams({ subject }); router.push(`/catalogue?${queryParams.toString()}`); }} - className="h-full cursor-pointer rounded-sm border-2 border-[#734DFF] bg-[#FFFFFF] text-black shadow-lg transition duration-150 ease-in-out hover:bg-[#EFEAFF] dark:border-[#36266D] dark:bg-[#171720] dark:text-white hover:dark:bg-[#262635]" + className={`h-full rounded-sm border-2 border-[#734DFF] bg-[#FFFFFF] text-black shadow-lg transition duration-150 ease-in-out dark:border-[#36266D] dark:bg-[#171720] dark:text-white ${ + !paperCount + ? "cursor-not-allowed opacity-60 hover:bg-[#FFFFFF] dark:hover:bg-[#171720]" + : "cursor-pointer hover:bg-[#EFEAFF] hover:dark:bg-[#262635]" + }`} >
-

+

{courseCode} -

+
(Papers available: {paperCount})
+
-

+

{courseName}

-
- {slots?.map((slotValue) => capsule(slotValue))} -
+ + {paperCount ? ( +
+ {slots?.map((slotValue) => capsule(slotValue))} +
+ ) : ( +
+ We will have papers for this soon! +
+ )}
); diff --git a/src/components/screens/Hero.tsx b/src/components/screens/Hero.tsx index b85e9dd..6062b87 100644 --- a/src/components/screens/Hero.tsx +++ b/src/components/screens/Hero.tsx @@ -11,8 +11,7 @@ const Hero = () => {
- - + {/*

Learn More

Date: Sat, 19 Jul 2025 00:43:25 +0530 Subject: [PATCH 6/7] added pin button to navbar --- src/components/Navbar.tsx | 9 ++++++++- src/components/SideBar.tsx | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index caa8cb7..79df546 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -6,6 +6,7 @@ import ModeToggle from "@/components/toggle-theme"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { ArrowDownLeftIcon } from "lucide-react"; +import { StarIcon } from "lucide-react"; function Navbar() { const pathname = usePathname(); @@ -27,10 +28,16 @@ function Navbar() { Papers + +
+ + Pinned Subjects +
+
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index f5abf12..c2c6162 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -54,19 +54,19 @@ function SideBar({ anskey: boolean, ) => void; }) { - const exams = filterOptions?.uniqueExams.map((exam) => ({ + const exams = filterOptions?.uniqueExams?.map((exam) => ({ label: exam, value: exam, })); - const slots = filterOptions?.uniqueSlots.map((slot) => ({ + const slots = filterOptions?.uniqueSlots?.map((slot) => ({ label: slot, value: slot, })); - const years = filterOptions?.uniqueYears.map((year) => ({ + const years = filterOptions?.uniqueYears?.map((year) => ({ label: year, value: year, })); - const semesters = filterOptions?.uniqueSemesters.map((semester) => ({ + const semesters = filterOptions?.uniqueSemesters?.map((semester) => ({ label: semester, value: semester, })); @@ -101,14 +101,14 @@ function SideBar({ @@ -117,7 +117,7 @@ function SideBar({ variant="outline" onClick={handleDownloadAll} disabled={selectedPapers.length === 0} - className="font-play border-2 border-black font-semibold hover:bg-slate-800 hover:text-white dark:border-[#434dba] dark:hover:border-white dark:hover:bg-slate-900" + className="border-2 border-black font-play font-semibold hover:bg-slate-800 hover:text-white dark:border-[#434dba] dark:hover:border-white dark:hover:bg-slate-900" > Download All ({selectedPapers.length}) @@ -130,7 +130,7 @@ function SideBar({
{ handleApplyFilters([], [], [], [], [], false); }} @@ -151,7 +151,7 @@ function SideBar({ !selectedAnswerKeyIncluded, ); }} - className={`font-play flex cursor-pointer rounded-full border-2 border-black px-2 py-1 text-xs font-semibold hover:bg-slate-800 hover:text-white ${selectedAnswerKeyIncluded ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} + className={`flex cursor-pointer rounded-full border-2 border-black px-2 py-1 font-play text-xs font-semibold hover:bg-slate-800 hover:text-white ${selectedAnswerKeyIncluded ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} > Answer Key Available
@@ -194,7 +194,7 @@ function SideBar({ ); } }} - className={`font-play mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedExams.includes(exam.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} + className={`mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 font-play text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedExams.includes(exam.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} > {exam.label}
@@ -236,7 +236,7 @@ function SideBar({ ); } }} - className={`font-play mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedSlots.includes(slot.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} + className={`mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 font-play text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedSlots.includes(slot.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} > {slot.label}
@@ -278,7 +278,7 @@ function SideBar({ ); } }} - className={`font-play mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedYears.includes(year.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} + className={`mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 font-play text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedYears.includes(year.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} > {year.label}
@@ -320,7 +320,7 @@ function SideBar({ ); } }} - className={`font-play mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedSemesters.includes(semester.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} + className={`mb-2 mr-2 flex h-fit cursor-pointer items-center rounded-full border-2 border-black px-2 py-1 font-play text-xs font-semibold hover:bg-slate-800 hover:text-white dark:hover:bg-slate-900 ${selectedSemesters.includes(semester.value) ? "border-[#B2B8FF] bg-[#B2B8FF] hover:border-black hover:bg-[#B2B8FF] dark:border-[#434dba] dark:bg-[#434dba] dark:hover:border-[white] dark:hover:bg-[#434dba]" : "bg-none hover:bg-[#B2B8FF] dark:border-white dark:hover:border-[#434dba]"}`} > {semester.label}
From ee177a8bc04e9e4908d54774d826a20f62b475f9 Mon Sep 17 00:00:00 2001 From: Advik-Gupta Date: Sat, 19 Jul 2025 01:25:45 +0530 Subject: [PATCH 7/7] added pin/unpin to subject page --- src/components/CatalogueContent.tsx | 52 +++++++++++++++++-- src/components/Searchbar/pinned-searchbar.tsx | 22 ++++---- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/components/CatalogueContent.tsx b/src/components/CatalogueContent.tsx index 68d305d..f181f95 100644 --- a/src/components/CatalogueContent.tsx +++ b/src/components/CatalogueContent.tsx @@ -13,6 +13,8 @@ import SideBar from "../components/SideBar"; import Error from "./Error"; import { Filter } from "lucide-react"; import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"; +import { StarIcon } from "lucide-react"; +import { StoredSubjects } from "@/interface"; export async function downloadFile(url: string, filename: string) { try { @@ -48,25 +50,51 @@ const CatalogueContent = () => { const [filterOptions, setFilterOptions] = useState(); const [filtersPulled, setFiltersPulled] = useState(false); const [appliedFilters, setAppliedFilters] = useState(false); + const [pinned, setPinned] = useState(false); // Set initial state from searchParams on client-side mount useEffect(() => { setIsMounted(true); if (searchParams) { - setSubject(searchParams.get("subject")); + const currentPinnedSubjects = JSON.parse( + localStorage.getItem("userSubjects") ?? "[]", + ) as StoredSubjects; + const subjectName = searchParams.get("subject"); + setSubject(subjectName); setSelectedExams(searchParams.get("exams")?.split(",") ?? []); setSelectedSlots(searchParams.get("slots")?.split(",") ?? []); setSelectedYears(searchParams.get("years")?.split(",") ?? []); setSelectedCampuses(searchParams.get("campus")?.split(",") ?? []); setSelectedSemesters(searchParams.get("semester")?.split(",") ?? []); setSelectedAnswerKeyIncluded(searchParams.get("answerkey") === "true"); + if (subjectName && Array.isArray(currentPinnedSubjects)) { + if (currentPinnedSubjects.includes(subjectName)) { + setPinned(true); + } else { + setPinned(false); + } + } } - }, [searchParams]); + }, [searchParams, pinned]); const filtersNotPulled = () => { setFiltersPulled(false); }; + const handlePinToggle = () => { + const current = !pinned; + setPinned(current); + + const saved = JSON.parse( + localStorage.getItem("userSubjects") ?? "[]", + ) as string[]; + const updated = current + ? [...new Set([...saved, subject])] + : saved.filter((s) => s !== subject); + + localStorage.setItem("userSubjects", JSON.stringify(updated)); + }; + // Fetch papers and apply filters useEffect(() => { if (!subject || !isMounted) return; @@ -295,11 +323,29 @@ const CatalogueContent = () => { +
+
+

+ {subject?.split("[")[1]?.replace("]", "")} +

+

+ {subject?.split(" [")[0]} +

+
+
+ +
+
+ {loading ? ( ) : papers.length > 0 ? (
{appliedFilters ? ( filteredPapers.length > 0 ? ( diff --git a/src/components/Searchbar/pinned-searchbar.tsx b/src/components/Searchbar/pinned-searchbar.tsx index 032020c..7246d13 100644 --- a/src/components/Searchbar/pinned-searchbar.tsx +++ b/src/components/Searchbar/pinned-searchbar.tsx @@ -48,10 +48,12 @@ function PinnedSearchBar({ localStorage.getItem("userSubjects") ?? "[]", ) as StoredSubjects; - if (currentPinnedSubjects.subjects.includes(suggestion)) { - setPinned(true); - } else { - setPinned(false); + if (suggestion && Array.isArray(currentPinnedSubjects)) { + if (currentPinnedSubjects.includes(suggestion)) { + setPinned(true); + } else { + setPinned(false); + } } setSuggestions([]); @@ -67,15 +69,17 @@ function PinnedSearchBar({ } }; - const hanglePinToggle = () => { + const handlePinToggle = () => { const current = !pinned; setPinned(current); + const saved = JSON.parse( localStorage.getItem("userSubjects") ?? "[]", - ) as StoredSubjects; + ) as string[]; const updated = current - ? [...new Set([...saved.subjects, searchText])] - : saved.subjects.filter((s: string) => s !== searchText); + ? [...new Set([...saved, searchText])] + : saved.filter((s) => s !== searchText); + localStorage.setItem("userSubjects", JSON.stringify(updated)); window.dispatchEvent(new Event("userSubjectsChanged")); }; @@ -131,7 +135,7 @@ function PinnedSearchBar({ )}
- +