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