Skip to content

Commit 0e04f73

Browse files
committed
carroussel ui on about page
1 parent e7ac850 commit 0e04f73

3 files changed

Lines changed: 138 additions & 100 deletions

File tree

backend/app.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -412,34 +412,32 @@ def serve_frontend(path):
412412
from flask import abort
413413
abort(404)
414414

415-
416-
# Serve static files
415+
# 1. Try to serve exact file match
417416
if path != "" and os.path.exists(os.path.join(static_folder, path)):
418417
return send_from_directory(static_folder, path)
419418

420-
# Check for built 404.html from Next.js (from pages/404.tsx)
421-
# Next.js static export creates 404.html in the output directory
422-
not_found_path = os.path.join(static_folder, '404.html')
423-
if path == '404' or path == '404.html':
424-
if os.path.exists(not_found_path):
425-
return send_file(not_found_path), 404
426-
# Also try 404/index.html structure (in case Next.js uses directory structure)
427-
not_found_dir_path = os.path.join(static_folder, '404', 'index.html')
428-
if os.path.exists(not_found_dir_path):
429-
return send_file(not_found_dir_path), 404
430-
431-
# Serve index.html for all other routes (SPA)
432-
# This allows client-side routing to handle invalid routes
433-
index_path = os.path.join(static_folder, 'index.html')
434-
if os.path.exists(index_path):
435-
return send_file(index_path)
419+
# 2. Try to serve as HTML (e.g. /about -> /about.html)
420+
# This is critical for Next.js static export which generates .html files
421+
if path != "" and not path.endswith('.html'):
422+
html_path = path + '.html'
423+
if os.path.exists(os.path.join(static_folder, html_path)):
424+
return send_from_directory(static_folder, html_path)
425+
426+
# 3. Try to serve index.html in directory (e.g. /blog/ -> /blog/index.html)
427+
index_in_dir = os.path.join(static_folder, path, 'index.html')
428+
if os.path.exists(index_in_dir):
429+
return send_file(index_in_dir)
436430

437-
# If we get here and it's a 404 request, return 404.html
431+
# 4. Handle 404 - Serve 404.html
432+
logger.info(f"Route not found: {path} - Serving 404.html")
433+
434+
# Check for built 404.html from Next.js
435+
not_found_path = os.path.join(static_folder, '404.html')
438436
if os.path.exists(not_found_path):
439437
return send_file(not_found_path), 404
440-
441-
from flask import abort
442-
abort(404)
438+
439+
# Fallback for 404 if 404.html is missing
440+
return jsonify({'success': False, 'errors': ['Page not found']}), 404
443441
else:
444442
logger.info("No static frontend folder - API only mode")
445443

frontend/components/About/FeatureCarousel.tsx

Lines changed: 116 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ export default function FeatureCarousel() {
6767
const requestRef = useRef<number>();
6868
const lastTimestampRef = useRef<number>();
6969
const timerRef = useRef<NodeJS.Timeout>();
70+
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
71+
72+
// Refs for Loop State to avoid Re-renders/Effect teardown
73+
const isPausedRef = useRef(false);
74+
const isSnappingRef = useRef(false);
75+
const isDraggingRef = useRef(false);
76+
const selectedFeatureRef = useRef<string | null>(null);
77+
78+
// Sync Refs
79+
useEffect(() => { isPausedRef.current = isPaused; }, [isPaused]);
80+
useEffect(() => { isSnappingRef.current = isSnapping; }, [isSnapping]);
81+
useEffect(() => { isDraggingRef.current = isDragging; }, [isDragging]);
82+
useEffect(() => { selectedFeatureRef.current = selectedFeature; }, [selectedFeature]);
7083

7184
// Mobile detection
7285
useEffect(() => {
@@ -95,26 +108,25 @@ export default function FeatureCarousel() {
95108
}, [totalSetWidth]);
96109

97110
// Snapping Logic - Precision Centering
98-
const snapToNearest = useCallback(() => {
111+
const snapToIndex = useCallback((index: number) => {
99112
if (!containerRef.current) return;
100113
setIsSnapping(true);
101-
const currentX = xRaw.get();
102114
const viewportWidth = containerRef.current.offsetWidth;
103115
const center = viewportWidth / 2;
104116

105-
const bestIndex = Math.round((center - currentX - (cardWidth / 2)) / itemWidth);
106-
const targetX = center - (bestIndex * itemWidth) - (cardWidth / 2);
117+
const targetX = center - (index * itemWidth) - (cardWidth / 2);
107118

108-
setCenteredIndex(bestIndex);
119+
setCenteredIndex(index);
109120

110121
animate(xRaw, targetX, {
111-
type: "tween",
112-
ease: [0.25, 1, 0.5, 1], // Custom smooth ease
113-
duration: 0.6,
122+
type: "spring",
123+
stiffness: 300,
124+
damping: 30,
125+
mass: 1,
114126
onComplete: () => {
115127
const finalX = xRaw.get();
116128
const wrappedX = wrapAround(finalX);
117-
if (wrappedX !== finalX) {
129+
if (Math.abs(wrappedX - finalX) > 1) { // Only reset if difference is significant
118130
xRaw.set(wrappedX);
119131
const newIndex = Math.round((center - wrappedX - (cardWidth / 2)) / itemWidth);
120132
setCenteredIndex(newIndex);
@@ -124,37 +136,29 @@ export default function FeatureCarousel() {
124136
});
125137
}, [xRaw, itemWidth, cardWidth, wrapAround]);
126138

139+
const snapToNearest = useCallback(() => {
140+
if (!containerRef.current) return;
141+
const currentX = xRaw.get();
142+
const viewportWidth = containerRef.current.offsetWidth;
143+
const center = viewportWidth / 2;
144+
const bestIndex = Math.round((center - currentX - (cardWidth / 2)) / itemWidth);
145+
snapToIndex(bestIndex);
146+
}, [xRaw, itemWidth, cardWidth, snapToIndex]);
147+
127148
const handleManualMove = useCallback((direction: 'left' | 'right') => {
128149
if (!containerRef.current) return;
129-
setIsSnapping(true);
130150
const currentX = xRaw.get();
131151
const viewportWidth = containerRef.current.offsetWidth;
132152
const center = viewportWidth / 2;
133153

134154
// Base the next move on the current visual position to ensure accuracy
135155
const currentIdx = Math.round((center - currentX - (cardWidth / 2)) / itemWidth);
136156
const nextIndex = direction === 'left' ? currentIdx - 1 : currentIdx + 1;
137-
const targetX = center - (nextIndex * itemWidth) - (cardWidth / 2);
138-
139-
setCenteredIndex(nextIndex);
140-
animate(xRaw, targetX, {
141-
type: "tween",
142-
ease: [0.25, 1, 0.5, 1],
143-
duration: 0.6,
144-
onComplete: () => {
145-
const finalX = xRaw.get();
146-
const wrappedX = wrapAround(finalX);
147-
if (wrappedX !== finalX) {
148-
xRaw.set(wrappedX);
149-
const newIndex = Math.round((center - wrappedX - (cardWidth / 2)) / itemWidth);
150-
setCenteredIndex(newIndex);
151-
}
152-
setIsSnapping(false);
153-
}
154-
});
155-
}, [xRaw, itemWidth, cardWidth, wrapAround]);
157+
snapToIndex(nextIndex);
158+
}, [xRaw, itemWidth, cardWidth, snapToIndex]);
156159

157160
// Animation Loop (Desktop Only)
161+
// Animation Loop (Desktop Only) - Optimized with Refs
158162
useEffect(() => {
159163
if (isMobile) return;
160164

@@ -163,7 +167,8 @@ export default function FeatureCarousel() {
163167
const deltaTime = timestamp - lastTimestampRef.current;
164168
lastTimestampRef.current = timestamp;
165169

166-
if (!isPaused && !selectedFeature && !isSnapping && !isDragging) {
170+
// Read directly from refs to avoid effect dependencies
171+
if (!isPausedRef.current && !selectedFeatureRef.current && !isSnappingRef.current && !isDraggingRef.current) {
167172
const currentX = xRaw.get();
168173
const nextX = currentX - (speed * deltaTime) / 1000;
169174
xRaw.set(wrapAround(nextX));
@@ -175,7 +180,8 @@ export default function FeatureCarousel() {
175180
return () => {
176181
if (requestRef.current) cancelAnimationFrame(requestRef.current);
177182
};
178-
}, [isPaused, selectedFeature, isSnapping, isDragging, wrapAround, xRaw, isMobile]);
183+
// Dependencies are minimal: only re-run if mobile state or structural layout changes
184+
}, [isMobile, wrapAround, xRaw]);
179185

180186
// Auto-timer for Mobile
181187
useEffect(() => {
@@ -217,12 +223,17 @@ export default function FeatureCarousel() {
217223
if (isMobile) return;
218224
setIsPaused(true);
219225
snapToNearest();
220-
}, [snapToNearest, isMobile]);
226+
}, [isMobile, snapToNearest]);
221227

222228
const handleMouseLeaveParent = useCallback(() => {
223229
if (isMobile) return;
230+
if (hoverTimeoutRef.current) {
231+
clearTimeout(hoverTimeoutRef.current);
232+
hoverTimeoutRef.current = null;
233+
}
224234
setIsPaused(false);
225235
setCenteredIndex(null);
236+
setHoveredCard(null);
226237
lastTimestampRef.current = undefined;
227238
}, [isMobile]);
228239

@@ -286,11 +297,30 @@ export default function FeatureCarousel() {
286297
ease: "easeOut",
287298
duration: 0.4
288299
}}
289-
onMouseEnter={() => !isMobile && setHoveredCard(idx)}
290-
onMouseLeave={() => !isMobile && setHoveredCard(null)}
300+
onMouseEnter={() => {
301+
if (!isMobile) {
302+
setHoveredCard(idx);
303+
// Debounce slightly to prevent erratic behavior, but keep it snappy
304+
if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current);
305+
hoverTimeoutRef.current = setTimeout(() => {
306+
if (!isDragging) {
307+
snapToIndex(idx);
308+
}
309+
}, 50); // Reduced from 150ms to 50ms for faster response
310+
}
311+
}}
312+
onMouseLeave={() => {
313+
if (!isMobile) {
314+
setHoveredCard(null);
315+
if (hoverTimeoutRef.current) {
316+
clearTimeout(hoverTimeoutRef.current);
317+
hoverTimeoutRef.current = null;
318+
}
319+
}
320+
}}
291321
whileHover={{
292322
scale: isMobile ? 1.0 : (isCentered ? 1.05 : 0.98),
293-
transition: { type: "tween", ease: "easeOut", duration: 0.2 }
323+
transition: { type: "spring", stiffness: 400, damping: 25 }
294324
}}
295325
onClick={() => setSelectedFeature(feature.key)}
296326
className={`relative shrink-0 rounded-[2.5rem] md:rounded-[3rem] p-6 md:p-10 overflow-hidden cursor-pointer bg-slate-950/40 border transition-all duration-300 ${isHovered || (isMobile && isCentered)
@@ -300,7 +330,7 @@ export default function FeatureCarousel() {
300330
style={{
301331
width: cardWidth,
302332
height: isMobile ? 380 : 440,
303-
backgroundColor: (isHovered || isMobile) ? `${featureColor}08` : 'rgba(255,255,255,0.03)',
333+
backgroundColor: (isHovered || isMobile) ? `${featureColor}08` : 'rgba(30, 41, 59, 0.4)', // More solid, no blur
304334
borderColor: (isHovered || isMobile) ? `${featureColor}99` : 'rgba(255,255,255,0.05)',
305335
boxShadow: (isHovered || isMobile) ? `0 25px 50px -12px ${featureColor}55` : 'none',
306336
['--tw-ring-color' as any]: (isHovered || (isMobile && isCentered)) ? `${featureColor}44` : 'transparent',
@@ -348,55 +378,65 @@ export default function FeatureCarousel() {
348378
</motion.div>
349379
</div>
350380

351-
{/* Glow Overlay */}
381+
{/* Simplified Glow Overlay */}
352382
<div
353383
className="absolute inset-0 opacity-0 group-hover:opacity-10 transition-opacity duration-700 pointer-events-none"
354-
style={{ background: `radial-gradient(circle at 50% 100%, ${featureColor}, transparent)` }}
384+
style={{
385+
background: isHovered ? `radial-gradient(circle at 50% 100%, ${featureColor}, transparent)` : 'none'
386+
}}
355387
/>
356388
</motion.div>
357389
);
358390
})}
359391
</motion.div>
360392
</div>
361393

362-
{/* Navigation Arrows */}
363-
<AnimatePresence>
364-
{(isPaused || isMobile) && !selectedFeature && (
365-
<div className="absolute inset-0 pointer-events-none z-50 flex items-center justify-between px-4 md:justify-center md:px-0">
366-
<div className={`relative ${isMobile ? 'flex w-full justify-between items-center h-full' : 'w-[300px] h-[440px]'}`}>
367-
<motion.button
368-
initial={{ opacity: 0, scale: 0.8 }}
369-
animate={{ opacity: 1, scale: 1, x: isMobile ? 0 : -85 }}
370-
exit={{ opacity: 0, scale: 0.8 }}
371-
onClick={(e) => {
372-
e.preventDefault();
373-
e.stopPropagation();
374-
handleManualMove('left');
375-
}}
376-
className={`${isMobile ? 'relative' : 'absolute left-0 top-1/2 -translate-y-1/2 -translate-x-full'
377-
} w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900/90 backdrop-blur-md border border-white/20 flex items-center justify-center text-white hover:text-white hover:bg-blue-600 hover:border-blue-400 transition-all shadow-2xl pointer-events-auto active:scale-90`}
378-
>
379-
<ChevronLeft size={isMobile ? 24 : 28} />
380-
</motion.button>
381-
382-
<motion.button
383-
initial={{ opacity: 0, scale: 0.8 }}
384-
animate={{ opacity: 1, scale: 1, x: isMobile ? 0 : 85 }}
385-
exit={{ opacity: 0, scale: 0.8 }}
386-
onClick={(e) => {
387-
e.preventDefault();
388-
e.stopPropagation();
389-
handleManualMove('right');
390-
}}
391-
className={`${isMobile ? 'relative' : 'absolute right-0 top-1/2 -translate-y-1/2 translate-x-full'
392-
} w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900/90 backdrop-blur-md border border-white/20 flex items-center justify-center text-white hover:text-white hover:bg-blue-600 hover:border-blue-400 transition-all shadow-2xl pointer-events-auto active:scale-90`}
393-
>
394-
<ChevronRight size={isMobile ? 24 : 28} />
395-
</motion.button>
396-
</div>
397-
</div>
398-
)}
399-
</AnimatePresence>
394+
{/* Navigation Arrows - Optimized visibility without unmounting */}
395+
<motion.div
396+
className="absolute inset-0 pointer-events-none z-50 flex items-center justify-between px-4 md:justify-center md:px-0"
397+
animate={{
398+
opacity: (isPaused || isMobile) && !selectedFeature ? 1 : 0
399+
}}
400+
transition={{ duration: 0.3 }}
401+
>
402+
<div className={`relative ${isMobile ? 'flex w-full justify-between items-center h-full' : 'w-[450px] h-[440px]'}`}>
403+
<motion.button
404+
initial={{ scale: 0.8 }}
405+
animate={{
406+
scale: (isPaused || isMobile) ? 1 : 0.8,
407+
x: isMobile ? 0 : -85,
408+
pointerEvents: (isPaused || isMobile) && !selectedFeature ? 'auto' : 'none'
409+
}}
410+
onClick={(e) => {
411+
e.preventDefault();
412+
e.stopPropagation();
413+
handleManualMove('left');
414+
}}
415+
className={`${isMobile ? 'relative' : 'absolute left-0 top-1/2 -translate-y-1/2'
416+
} w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900 border border-white/20 flex items-center justify-center text-white hover:bg-blue-600 transition-all shadow-2xl active:scale-90`}
417+
>
418+
<ChevronLeft size={isMobile ? 24 : 28} />
419+
</motion.button>
420+
421+
<motion.button
422+
initial={{ scale: 0.8 }}
423+
animate={{
424+
scale: (isPaused || isMobile) ? 1 : 0.8,
425+
x: isMobile ? 0 : 85,
426+
pointerEvents: (isPaused || isMobile) && !selectedFeature ? 'auto' : 'none'
427+
}}
428+
onClick={(e) => {
429+
e.preventDefault();
430+
e.stopPropagation();
431+
handleManualMove('right');
432+
}}
433+
className={`${isMobile ? 'relative' : 'absolute right-0 top-1/2 -translate-y-1/2'
434+
} w-10 h-10 md:w-12 md:h-12 rounded-xl bg-slate-900 border border-white/20 flex items-center justify-center text-white hover:bg-blue-600 transition-all shadow-2xl active:scale-90`}
435+
>
436+
<ChevronRight size={isMobile ? 24 : 28} />
437+
</motion.button>
438+
</div>
439+
</motion.div>
400440
</div>
401441

402442
{/* Feature Detailed Modal */}

0 commit comments

Comments
 (0)