109109 .filter-item : hover {background : var (--gray );}
110110 .filter-item .active {background : var (--primary ); color : white;}
111111 .clear-search {position : absolute; right : 2.5rem ; top : 50% ; transform : translateY (-50% ); color : # 6c757d ; cursor : pointer; font-size : 0.8rem ; display : none;}
112+
113+ /* Skeleton loader styles */
114+ .skeleton {
115+ background : linear-gradient (90deg , var (--card ) 0% , var (--gray ) 50% , var (--card ) 100% );
116+ background-size : 200% 100% ;
117+ animation : skeleton-loading 1.5s infinite;
118+ border-radius : var (--radius );
119+ }
120+
121+ @keyframes skeleton-loading {
122+ 0% { background-position : 200% 0 ; }
123+ 100% { background-position : -200% 0 ; }
124+ }
125+
126+ .skeleton-card {
127+ background : var (--card );
128+ border-radius : var (--radius );
129+ box-shadow : var (--shadow );
130+ overflow : hidden;
131+ height : 100% ;
132+ display : flex;
133+ flex-direction : column;
134+ }
135+
136+ .skeleton-img {
137+ width : 100% ;
138+ aspect-ratio : 16 / 10 ;
139+ }
140+
141+ .skeleton-title {
142+ height : 20px ;
143+ width : 80% ;
144+ margin : 1rem 1rem 0.5rem 1rem ;
145+ }
146+
147+ .skeleton-desc {
148+ height : 12px ;
149+ width : 90% ;
150+ margin : 0.5rem 1rem ;
151+ }
152+
153+ .skeleton-desc-short {
154+ width : 60% ;
155+ }
156+
157+ .skeleton-btn {
158+ height : 36px ;
159+ width : 100px ;
160+ margin : auto 1rem 1rem 1rem ;
161+ }
162+
163+ .skeleton-stat-card {
164+ background : var (--card );
165+ border-radius : var (--radius );
166+ padding : 1rem ;
167+ box-shadow : var (--shadow );
168+ min-width : 150px ;
169+ text-align : center;
170+ flex-grow : 1 ;
171+ max-width : 220px ;
172+ }
173+
174+ .skeleton-stat-number {
175+ height : 28px ;
176+ width : 60% ;
177+ margin : 0 auto 0.5rem auto;
178+ }
179+
180+ .skeleton-stat-label {
181+ height : 16px ;
182+ width : 80% ;
183+ margin : 0 auto;
184+ }
185+
112186 @media (max-width : 768px ) {
113187 .header-content {flex-wrap : wrap; height : auto; padding : 1rem ;}
114188 .search-bar {width : 100% ; margin-top : 1rem ; order : 3 ;}
146220 .back-to-top { background-color : var (--primary ); color : white; }
147221 .pill .game { background : rgba (77 , 132 , 163 , 0.3 ); color : var (--primary ); }
148222 .pill .tool { background : rgba (245 , 168 , 73 , 0.3 ); color : var (--secondary ); }
223+ .skeleton { background : linear-gradient (90deg , var (--card ) 0% , var (--gray ) 50% , var (--card ) 100% ); }
224+ .skeleton-card { background-color : var (--card ); }
225+ .skeleton-stat-card { background-color : var (--card ); }
149226 }
150227
151228 .svg-inline--fa {
220297</ div >
221298
222299< main class ="container ">
223- < div class ="stats " id ="statsContainer "> </ div >
300+ < div class ="stats " id ="statsContainer ">
301+ <!-- Skeleton stats will be placed here -->
302+ </ div >
224303
225304 < section class ="main-section ">
226305 < div class ="section-header ">
227306 < h2 class ="section-title " id ="mainTitle "> Featured Games & Tools</ h2 >
228307 </ div >
229- < div class ="grid-view " id ="mainGrid "> </ div >
308+ < div class ="grid-view " id ="mainGrid ">
309+ <!-- Skeleton cards will be placed here -->
310+ </ div >
230311 < div class ="loading " id ="loadingIndicator ">
231312 < div class ="spinner "> </ div >
232313 < p > Loading more items...</ p >
@@ -295,6 +376,60 @@ <h3>No results found</h3>
295376 const itemsPerPage = 24 ;
296377 const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000 ;
297378
379+ // Initialize skeleton loaders
380+ function createSkeletonStatCard ( ) {
381+ return `
382+ <div class="skeleton-stat-card">
383+ <div class="skeleton skeleton-stat-number"></div>
384+ <div class="skeleton skeleton-stat-label"></div>
385+ </div>
386+ ` ;
387+ }
388+
389+ function createSkeletonCard ( ) {
390+ return `
391+ <div class="skeleton-card">
392+ <div class="skeleton skeleton-img"></div>
393+ <div class="skeleton skeleton-title"></div>
394+ <div class="skeleton skeleton-desc"></div>
395+ <div class="skeleton skeleton-desc skeleton-desc-short"></div>
396+ <div class="skeleton skeleton-btn"></div>
397+ </div>
398+ ` ;
399+ }
400+
401+ function initSkeletons ( ) {
402+ // Init stats skeletons
403+ let statsSkeletons = '' ;
404+ for ( let i = 0 ; i < 5 ; i ++ ) {
405+ statsSkeletons += createSkeletonStatCard ( ) ;
406+ }
407+ statsContainer . innerHTML = statsSkeletons ;
408+
409+ // Init card skeletons - calculate how many fit in the viewport
410+ const viewportWidth = Math . max ( document . documentElement . clientWidth || 0 , window . innerWidth || 0 ) ;
411+ const viewportHeight = Math . max ( document . documentElement . clientHeight || 0 , window . innerHeight || 0 ) ;
412+
413+ const cardWidth = isMobile ? 160 : 220 ;
414+ const cardHeight = 280 ;
415+ const gridGap = 20 ;
416+
417+ const cardsPerRow = Math . floor ( viewportWidth / ( cardWidth + gridGap ) ) ;
418+ const rowsVisible = Math . ceil ( ( viewportHeight - 300 ) / ( cardHeight + gridGap ) ) ;
419+
420+ // Create at least 8 cards or enough to fill viewport plus one row
421+ const skeletonCount = Math . max ( 8 , cardsPerRow * ( rowsVisible + 1 ) ) ;
422+
423+ let gridSkeletons = '' ;
424+ for ( let i = 0 ; i < skeletonCount ; i ++ ) {
425+ gridSkeletons += createSkeletonCard ( ) ;
426+ }
427+ mainGrid . innerHTML = gridSkeletons ;
428+ }
429+
430+ // Call immediately to show skeletons before data loads
431+ initSkeletons ( ) ;
432+
298433 // Category mapping function
299434 function getCategory ( item ) {
300435 const title = item . title . toLowerCase ( ) ;
@@ -712,4 +847,4 @@ <h3 class="card-title" title="${item.title}">${item.title}</h3>
712847<!--<script src="util/chatbot/chatbot.js"></script>-->
713848< script data-goatcounter ="https://gptgames.goatcounter.com/count " async src ="//gc.zgo.at/count.js "> </ script >
714849</ body >
715- </ html >
850+ </ html >
0 commit comments