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

View File

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

View File

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