new
All checks were successful
Deploy to Server / deploy (push) Successful in 35s

This commit is contained in:
2026-02-13 23:59:02 +08:00
parent b773f30e0d
commit 84e49740f3
3 changed files with 38 additions and 19 deletions

View File

@@ -1,16 +1,16 @@
import React, { useState } from 'react';
import React, { useState, useRef, useLayoutEffect } from 'react';
import { motion } from 'framer-motion';
import { CalendarOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import styles from './activity.module.less';
import { hoverScale } from '../../animation';
//
const ActivityCard = ({ activity }) => {
const navigate = useNavigate();
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
const imgRef = useRef(null);
const handleCardClick = () => {
navigate(`/activity/${activity.id}`);
@@ -33,6 +33,13 @@ const ActivityCard = ({ activity }) => {
? 'https://via.placeholder.com/600x400?text=No+Image'
: (activity.display_banner_url || activity.banner_url || activity.cover_image || 'https://via.placeholder.com/600x400');
// Check if image is already loaded (cached) to prevent flashing
useLayoutEffect(() => {
if (imgRef.current && imgRef.current.complete) {
setIsLoaded(true);
}
}, [imgSrc]);
return (
<motion.div
className={styles.activityCard}
@@ -43,7 +50,7 @@ const ActivityCard = ({ activity }) => {
style={{ willChange: 'transform' }}
>
<div className={styles.imageContainer}>
{/* Placeholder / Skeleton Background */}
{/* Placeholder Background - Always visible behind the image */}
<div
style={{
position: 'absolute',
@@ -51,26 +58,32 @@ const ActivityCard = ({ activity }) => {
left: 0,
width: '100%',
height: '100%',
backgroundColor: '#f0f0f0',
opacity: isLoaded ? 0 : 1,
transition: 'opacity 0.5s ease'
backgroundColor: '#f5f5f5',
zIndex: 0,
}}
/>
<motion.img
<img
ref={imgRef}
src={imgSrc}
alt={activity.title}
initial={{ opacity: 0 }}
animate={{ opacity: isLoaded ? 1 : 0 }}
transition={{ duration: 0.5 }}
style={{
position: 'relative',
zIndex: 1,
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: isLoaded ? 1 : 0,
transition: 'opacity 0.3s ease-out'
}}
onLoad={() => setIsLoaded(true)}
onError={() => {
setHasError(true);
setIsLoaded(true); // Show placeholder image
setIsLoaded(true);
}}
loading="lazy"
/>
<div className={styles.overlay}>
<div className={styles.overlay} style={{ zIndex: 2 }}>
<div className={styles.statusTag}>
{activity.status || getStatus(activity.start_time)}
</div>

View File

@@ -66,20 +66,25 @@ const ActivityList = () => {
User said: "Activity only shows one, and in the form of a sliding page"
*/}
<div className={styles.desktopCarousel}>
<AnimatePresence mode='wait'>
<AnimatePresence>
<motion.div
key={currentIndex}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.5 }}
style={{ width: '100%' }}
initial={{ x: '100%' }}
animate={{ x: 0, zIndex: 1 }}
exit={{ x: '-100%', zIndex: 0 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
style={{
width: '100%',
position: 'absolute',
top: 0,
left: 0,
}}
>
<ActivityCard activity={activities[currentIndex]} />
</motion.div>
</AnimatePresence>
<div className={styles.dots}>
<div className={styles.dots} style={{ position: 'absolute', bottom: '10px', width: '100%', zIndex: 10 }}>
{activities.map((_, idx) => (
<span
key={idx}

View File

@@ -63,6 +63,7 @@
.desktopCarousel {
position: relative;
width: 100%;
height: 440px; /* 400px card + space for dots */
overflow: hidden;
@media (max-width: 768px) {