This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user