Skip to content

Commit 1209a71

Browse files
merge
1 parent e4c0a63 commit 1209a71

1 file changed

Lines changed: 268 additions & 0 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import axios from "axios";
5+
import { type IUpcomingPaper } from "@/interface";
6+
import UpcomingPaper from "./UpcomingPaper";
7+
import {
8+
Carousel,
9+
CarouselContent,
10+
CarouselItem,
11+
CarouselNext,
12+
CarouselPrevious,
13+
} from "@/components/ui/carousel";
14+
import Autoplay from "embla-carousel-autoplay";
15+
import { chunkArray } from "@/lib/utils/array";
16+
import { type StoredSubjects } from "@/interface";
17+
import SkeletonPaperCard from "./SkeletonPaperCard";
18+
import PinnedModal from "./ui/PinnedModal";
19+
20+
function PinnedPapersCarousel() {
21+
const [isLoading, setIsLoading] = useState(true);
22+
const [chunkSize, setChunkSize] = useState<number>(4);
23+
const [displayPapers, setDisplayPapers] = useState<IUpcomingPaper[]>([]);
24+
useEffect(() => {
25+
const handleResize = () => {
26+
if (window.innerWidth <= 540) {
27+
setChunkSize(2);
28+
} else if (window.innerWidth <= 920) {
29+
setChunkSize(4);
30+
} else {
31+
setChunkSize(8);
32+
}
33+
};
34+
35+
handleResize(); // initialize
36+
window.addEventListener("resize", handleResize);
37+
return () => window.removeEventListener("resize", handleResize);
38+
}, []);
39+
40+
const chunkedPapers = chunkArray(displayPapers, chunkSize);
41+
42+
if (chunkedPapers.length > 0) {
43+
const lastChunkIndex = chunkedPapers.length - 1;
44+
if ((chunkedPapers[lastChunkIndex]?.length ?? 0) < chunkSize) {
45+
chunkedPapers[lastChunkIndex] = [
46+
...(chunkedPapers[lastChunkIndex] ?? []),
47+
{ subject: "add_subject_button", slots: [] } as IUpcomingPaper,
48+
];
49+
} else {
50+
chunkedPapers.push([
51+
{ subject: "add_subject_button", slots: [] } as IUpcomingPaper,
52+
]);
53+
}
54+
}
55+
56+
const fetchPapers = async () => {
57+
try {
58+
setIsLoading(true);
59+
60+
const storedSubjects = JSON.parse(
61+
localStorage.getItem("userSubjects") ?? "[]",
62+
) as StoredSubjects;
63+
64+
const response = await axios.post<{ subject: string; slots: string[] }[]>(
65+
"/api/user-papers",
66+
storedSubjects,
67+
);
68+
69+
const fetchedPapers = response.data;
70+
71+
const fetchedSubjectsSet = new Set(
72+
fetchedPapers.map((paper) => paper.subject),
73+
);
74+
75+
const storedSubjectsArray = Array.isArray(storedSubjects)
76+
? storedSubjects
77+
: [];
78+
const missingSubjects = storedSubjectsArray
79+
.filter((subject: string) => !fetchedSubjectsSet.has(subject))
80+
.map((subject: string) => ({
81+
subject,
82+
slots: [],
83+
})) as { subject: string; slots: string[] }[];
84+
85+
const allDisplayPapers = [...fetchedPapers, ...missingSubjects];
86+
87+
allDisplayPapers.sort((a, b) => {
88+
const aIndex = storedSubjects.indexOf(a.subject);
89+
const bIndex = storedSubjects.indexOf(b.subject);
90+
91+
return (
92+
(aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) -
93+
(bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex)
94+
);
95+
});
96+
97+
setDisplayPapers(allDisplayPapers);
98+
} catch (error) {
99+
console.error("Failed to fetch papers:", error);
100+
} finally {
101+
setIsLoading(false);
102+
}
103+
};
104+
105+
useEffect(() => {
106+
void fetchPapers();
107+
}, []);
108+
109+
useEffect(() => {
110+
const handleSubjectsChange = () => {
111+
void (async () => {
112+
try {
113+
const storedSubjects = JSON.parse(
114+
localStorage.getItem("userSubjects") ?? "[]",
115+
) as StoredSubjects;
116+
117+
const response = await axios.post<
118+
{ subject: string; slots: string[] }[]
119+
>("/api/user-papers", storedSubjects);
120+
121+
const fetchedPapers = response.data;
122+
123+
const fetchedSubjectsSet = new Set(
124+
fetchedPapers.map((paper) => paper.subject),
125+
);
126+
127+
const storedSubjectsArray = Array.isArray(storedSubjects)
128+
? storedSubjects
129+
: [];
130+
const missingSubjects = storedSubjectsArray
131+
.filter((subject: string) => !fetchedSubjectsSet.has(subject))
132+
.map((subject: string) => ({
133+
subject,
134+
slots: [],
135+
})) as { subject: string; slots: string[] }[];
136+
137+
const allDisplayPapers = [...fetchedPapers, ...missingSubjects];
138+
139+
allDisplayPapers.sort((a, b) => {
140+
const aIndex = storedSubjects.indexOf(a.subject);
141+
const bIndex = storedSubjects.indexOf(b.subject);
142+
143+
return (
144+
(aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex) -
145+
(bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex)
146+
);
147+
});
148+
149+
setDisplayPapers(allDisplayPapers);
150+
} catch (error) {
151+
console.error("Failed to fetch papers:", error);
152+
}
153+
})();
154+
};
155+
156+
window.addEventListener("userSubjectsChanged", handleSubjectsChange);
157+
158+
return () => {
159+
window.removeEventListener("userSubjectsChanged", handleSubjectsChange);
160+
};
161+
}, []);
162+
163+
const plugins = [Autoplay({ delay: 8000, stopOnInteraction: true })];
164+
165+
return (
166+
<div className="mt-8 px-4 md:mt-4">
167+
<div className="">
168+
{displayPapers.length > 0 ? (
169+
<Carousel
170+
opts={{
171+
align: "start",
172+
loop: true,
173+
}}
174+
plugins={plugins}
175+
className="w-full"
176+
>
177+
{(() => {
178+
const totalItems = displayPapers.length + 1;
179+
const needsNav = totalItems > chunkSize;
180+
return needsNav ? (
181+
<div className="relative mt-4 flex justify-end gap-4">
182+
<CarouselPrevious className="relative" />
183+
<CarouselNext className="relative" />
184+
</div>
185+
) : null;
186+
})()}
187+
<CarouselContent>
188+
{isLoading ? (
189+
<CarouselItem
190+
className={`grid ${
191+
chunkSize === 2
192+
? "grid-cols-1 grid-rows-2"
193+
: chunkSize === 4
194+
? "grid-cols-2 grid-rows-2"
195+
: "grid-cols-4"
196+
} gap-4 lg:auto-rows-fr`}
197+
>
198+
<SkeletonPaperCard length={chunkSize} />
199+
</CarouselItem>
200+
) : (
201+
chunkedPapers.map((paperGroup, index) => {
202+
const columns = chunkSize === 2 ? 1 : chunkSize === 4 ? 2 : 4;
203+
const rows = Math.max(1, Math.ceil(paperGroup.length / columns));
204+
const placeholdersNeeded = columns * rows - paperGroup.length;
205+
return (
206+
<CarouselItem
207+
key={`carousel-item-${index}`}
208+
className={`grid ${
209+
columns === 1
210+
? "grid-cols-1"
211+
: columns === 2
212+
? "grid-cols-2"
213+
: "grid-cols-4"
214+
} ${rows === 1 ? "grid-rows-1" : "grid-rows-2"} gap-4 lg:auto-rows-fr`}
215+
>
216+
{paperGroup.map((paper, subIndex) =>
217+
paper.subject === "add_subject_button" ? (
218+
<div
219+
key={subIndex}
220+
className="h-full rounded-sm border border-dashed border-[#734DFF] bg-[#FFFFFF] font-bold hover:bg-[#EFEAFF] dark:border-[#36266D] dark:bg-transparent dark:hover:bg-[#1A1823]"
221+
>
222+
<PinnedModal
223+
triggerName={"Add Subjects"}
224+
page={"Carousel"}
225+
/>
226+
</div>
227+
) : (
228+
<div key={subIndex} className="h-full">
229+
<UpcomingPaper
230+
subject={paper.subject}
231+
slots={paper.slots}
232+
/>
233+
</div>
234+
),
235+
)}
236+
237+
{Array.from({ length: placeholdersNeeded }).map(
238+
(_, placeholderIndex) => (
239+
<div
240+
key={`placeholder-${placeholderIndex}`}
241+
className="invisible h-full"
242+
></div>
243+
),
244+
)}
245+
</CarouselItem>
246+
);
247+
})
248+
)}
249+
</CarouselContent>
250+
</Carousel>
251+
) : (
252+
<div
253+
className={`relative flex flex-col items-center justify-center gap-4 text-center font-bold`}
254+
>
255+
Start pinning subjects for quick and easy access.
256+
<div className="flex h-8 items-center gap-1 rounded-full border border-[#3A3745] bg-[#e8e9ff] px-2.5 py-1 text-xs font-semibold text-gray-700 transition hover:bg-slate-50 dark:bg-black dark:text-white dark:hover:bg-[#1A1823] sm:h-9 sm:gap-2 sm:px-3.5 sm:py-1.5 sm:text-sm md:h-10 md:px-4 md:py-2 md:text-base">
257+
<span className="truncate">
258+
<PinnedModal />
259+
</span>
260+
</div>
261+
</div>
262+
)}
263+
</div>
264+
</div>
265+
);
266+
}
267+
268+
export default PinnedPapersCarousel;

0 commit comments

Comments
 (0)