|
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 placeholdersNeeded = |
203 | | - (chunkSize - paperGroup.length) % chunkSize; |
204 | | - return ( |
205 | | - <CarouselItem |
206 | | - key={`carousel-item-${index}`} |
207 | | - className={`grid ${ |
208 | | - chunkSize === 2 |
209 | | - ? "grid-cols-1 grid-rows-2" |
210 | | - : chunkSize === 4 |
211 | | - ? "grid-cols-2 grid-rows-2" |
212 | | - : "grid-cols-4" |
213 | | - } gap-4 lg:auto-rows-fr`} |
214 | | - > |
215 | | - {paperGroup.map((paper, subIndex) => |
216 | | - paper.subject === "add_subject_button" ? ( |
217 | | - <div |
218 | | - key={subIndex} |
219 | | - 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]" |
220 | | - > |
221 | | - <PinnedModal |
222 | | - triggerName={"Add Subjects"} |
223 | | - page={"Carousel"} |
224 | | - /> |
225 | | - </div> |
226 | | - ) : ( |
227 | | - <div key={subIndex} className="h-full"> |
228 | | - <UpcomingPaper |
229 | | - subject={paper.subject} |
230 | | - slots={paper.slots} |
231 | | - /> |
232 | | - </div> |
233 | | - ), |
234 | | - )} |
235 | | - |
236 | | - {Array.from({ length: placeholdersNeeded }).map( |
237 | | - (_, placeholderIndex) => ( |
238 | | - <div |
239 | | - key={`placeholder-${placeholderIndex}`} |
240 | | - className="invisible h-full" |
241 | | - ></div> |
242 | | - ), |
243 | | - )} |
244 | | - </CarouselItem> |
245 | | - ); |
246 | | - }) |
247 | | - )} |
248 | | - </CarouselContent> |
249 | | - </Carousel> |
250 | | - ) : ( |
251 | | - <div |
252 | | - className={`relative flex flex-col items-center justify-center gap-4 text-center font-bold`} |
253 | | - > |
254 | | - Start pinning subjects for quick and easy access. |
255 | | - <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"> |
256 | | - <span className="truncate"> |
257 | | - <PinnedModal /> |
258 | | - </span> |
259 | | - </div> |
260 | | - </div> |
261 | | - )} |
262 | | - </div> |
263 | | - </div> |
264 | | - ); |
265 | | -} |
266 | | - |
267 | | -export default PinnedPapersCarousel; |
0 commit comments