From 814b2e846cfe3b10fdd8795ec56934f88ac5911d Mon Sep 17 00:00:00 2001 From: Advik-Gupta Date: Tue, 4 Nov 2025 00:50:55 +0530 Subject: [PATCH] make pinned subjects re-order by dragging --- src/app/api/user-papers/route.ts | 2 +- src/components/PinnedPapersCarousel.tsx | 249 +++++++++++++----------- src/components/ui/PinnedModal.tsx | 232 ++++++++++++++++------ src/interface.ts | 5 +- 4 files changed, 306 insertions(+), 182 deletions(-) diff --git a/src/app/api/user-papers/route.ts b/src/app/api/user-papers/route.ts index f3a9c67..8d2b9df 100644 --- a/src/app/api/user-papers/route.ts +++ b/src/app/api/user-papers/route.ts @@ -31,4 +31,4 @@ export async function POST(req: Request) { { status: 500 }, ); } -} \ No newline at end of file +} diff --git a/src/components/PinnedPapersCarousel.tsx b/src/components/PinnedPapersCarousel.tsx index 8a6309a..cbf448b 100644 --- a/src/components/PinnedPapersCarousel.tsx +++ b/src/components/PinnedPapersCarousel.tsx @@ -23,10 +23,9 @@ function PinnedPapersCarousel() { const [displayPapers, setDisplayPapers] = useState([]); useEffect(() => { const handleResize = () => { - if(window.innerWidth <= 540){ + if (window.innerWidth <= 540) { setChunkSize(2); - } - else if (window.innerWidth <= 920) { + } else if (window.innerWidth <= 920) { setChunkSize(4); } else { setChunkSize(8); @@ -48,7 +47,9 @@ function PinnedPapersCarousel() { { subject: "add_subject_button", slots: [] } as IUpcomingPaper, ]; } else { - chunkedPapers.push([{ subject: "add_subject_button", slots: [] } as IUpcomingPaper]); + chunkedPapers.push([ + { subject: "add_subject_button", slots: [] } as IUpcomingPaper, + ]); } } @@ -96,42 +97,41 @@ function PinnedPapersCarousel() { }, []); useEffect(() => { - const handleSubjectsChange = () => { - void (async () => { - try { - 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); - } - })(); - }; + const handleSubjectsChange = () => { + void (async () => { + try { + 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); + } + })(); + }; window.addEventListener("userSubjectsChanged", handleSubjectsChange); @@ -143,83 +143,102 @@ function PinnedPapersCarousel() { const plugins = [Autoplay({ delay: 8000, stopOnInteraction: true })]; return ( -
+
- {displayPapers.length > 0 ? - - {(() => { - const totalItems = displayPapers.length + 1; - const needsNav = totalItems > chunkSize; - return needsNav ? ( -
- - -
- ) : null; - })()} - - {isLoading ? ( - - - - ) : ( - chunkedPapers.map((paperGroup, index) => { - const placeholdersNeeded = (chunkSize - paperGroup.length) % chunkSize; - return ( - - {paperGroup.map((paper, subIndex) => ( - paper.subject === "add_subject_button" ? -
- -
- : -
- -
- ))} - - {Array.from({ length: placeholdersNeeded }).map( - (_, placeholderIndex) => ( -
- ), - )} -
- ); - }) - )} -
-
: -
- Start pinning subjects for quick and easy access. -
- - - + {displayPapers.length > 0 ? ( + + {(() => { + const totalItems = displayPapers.length + 1; + const needsNav = totalItems > chunkSize; + return needsNav ? ( +
+ + +
+ ) : null; + })()} + + {isLoading ? ( + + + + ) : ( + chunkedPapers.map((paperGroup, index) => { + const placeholdersNeeded = + (chunkSize - paperGroup.length) % chunkSize; + return ( + + {paperGroup.map((paper, subIndex) => + paper.subject === "add_subject_button" ? ( +
+ +
+ ) : ( +
+ +
+ ), + )} + + {Array.from({ length: placeholdersNeeded }).map( + (_, placeholderIndex) => ( +
+ ), + )} +
+ ); + }) + )} +
+
+ ) : ( +
+ Start pinning subjects for quick and easy access. +
+ + + +
-
} + )}
); diff --git a/src/components/ui/PinnedModal.tsx b/src/components/ui/PinnedModal.tsx index 077c25d..b977941 100644 --- a/src/components/ui/PinnedModal.tsx +++ b/src/components/ui/PinnedModal.tsx @@ -4,52 +4,128 @@ import axios from "axios"; import { Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog" +} from "@/components/ui/dialog"; import SearchBar from "@/components/Searchbar/searchbar"; import { type IUpcomingPaper } from "@/interface"; -import { StoredSubjects } from "@/interface"; +import { type StoredSubjects } from "@/interface"; import { useState, useEffect } from "react"; -import { Pin, PinOff } from "lucide-react"; -import { Plus } from "lucide-react"; +import { Pin, PinOff, Plus, GripVertical } from "lucide-react"; +import { + DndContext, + closestCenter, + PointerSensor, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + useSortable, + verticalListSortingStrategy, + arrayMove, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +const SortableItem = ({ + id, + subject, + onUnpin, +}: { + id: string; + subject: string; + onUnpin: (subject: string) => void; +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + return ( +
+
+ +
+ {subject} + +
+ ); +}; -const PinnedModal = ({triggerName = "Pin Subjects", page = "Navbar"} : {triggerName? : string, page? : string}) => { +const PinnedModal = ({ + triggerName = "Pin Subjects", + page = "Navbar", +}: { + triggerName?: string; + page?: string; +}) => { const [displayPapers, setDisplayPapers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [open, setOpen] = useState(false); + const sensors = useSensors(useSensor(PointerSensor)); + 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[] }[]; - + .map((subject: string) => ({ subject, slots: [] })) as { + subject: string; + slots: string[]; + }[]; const allDisplayPapers = [...fetchedPapers, ...missingSubjects]; + allDisplayPapers.sort((a, b) => { + const aIndex = storedSubjects.indexOf(a.subject); + const bIndex = storedSubjects.indexOf(b.subject); + + return ( + (aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) - + (bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex) + ); + }); + setDisplayPapers(allDisplayPapers); } catch (error) { console.error("Failed to fetch papers:", error); @@ -59,70 +135,102 @@ const PinnedModal = ({triggerName = "Pin Subjects", page = "Navbar"} : {triggerN }; const unpinSubject = (subjectToRemove: string) => { - const updatedSubjects = (JSON.parse(localStorage.getItem("userSubjects") ?? "[]") as string[]).filter( - (subj) => subj !== subjectToRemove - ); + const updatedSubjects = ( + JSON.parse(localStorage.getItem("userSubjects") ?? "[]") as string[] + ).filter((subj) => subj !== subjectToRemove); localStorage.setItem("userSubjects", JSON.stringify(updatedSubjects)); - setDisplayPapers((prev) => prev.filter((paper) => paper.subject !== subjectToRemove)); + setDisplayPapers((prev) => + prev.filter((paper) => paper.subject !== subjectToRemove), + ); window.dispatchEvent(new Event("userSubjectsChanged")); window.dispatchEvent(new Event("updatePapers")); }; + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (over && active.id !== over.id) { + setDisplayPapers((items) => { + const oldIndex = items.findIndex((i) => i.subject === active.id); + const newIndex = items.findIndex((i) => i.subject === over.id); + const newOrder = arrayMove(items, oldIndex, newIndex); + localStorage.setItem( + "userSubjects", + JSON.stringify(newOrder.map((p) => p.subject)), + ); + return newOrder; + }); + } + }; + useEffect(() => { void fetchPapers(); }, []); return ( - { - setOpen(isOpen); - if (isOpen) { - void fetchPapers(); - }else{ - window.dispatchEvent(new Event("userSubjectsChanged")); - window.dispatchEvent(new Event("updatePapers")); - } - }}> - {page === "Navbar" ? - - - {triggerName} - : - - - {triggerName} - - } - + { + setOpen(isOpen); + if (isOpen) { + void fetchPapers(); + } else { + window.dispatchEvent(new Event("userSubjectsChanged")); + window.dispatchEvent(new Event("updatePapers")); + } + }} + > + {page === "Navbar" ? ( + + + {triggerName} + + ) : ( + + + {triggerName} + + )} + - Quick Access to This Semester's Subjects + + Quick Access to This Semester's Subjects +
- +
-
+
{displayPapers.length > 0 ? ( - displayPapers.map((item, index) => ( -
+ p.subject)} + strategy={verticalListSortingStrategy} > - {item.subject} - -
- )) + {displayPapers.map((item) => ( + + ))} + + ) : ( -

Start pinning subjects for quick and easy access.

+

+ Start pinning subjects for quick and easy access. +

)}
@@ -130,7 +238,7 @@ const PinnedModal = ({triggerName = "Pin Subjects", page = "Navbar"} : {triggerN
- ) -} + ); +}; -export default PinnedModal \ No newline at end of file +export default PinnedModal; diff --git a/src/interface.ts b/src/interface.ts index f166706..eccb7d3 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -9,7 +9,6 @@ export interface IUpcomingSubject { slots: string[]; } - export interface PostRequestBody { tags: string; } @@ -148,9 +147,7 @@ export interface Filters { unique_semesters: string[]; } -export interface StoredSubjects { - subjects: string[]; -} +export type StoredSubjects = string[]; export interface TransformedPaper { subject: string;