diff --git a/backend/.repl_history b/backend/.repl_history index ac4c158..5c021f3 100644 --- a/backend/.repl_history +++ b/backend/.repl_history @@ -1,3 +1,5 @@ +.cli users:list +.cli users:create --email test@radziel.com --username radziel --password QWEqwe123 --first Radek --last Gierwialo .cli users:verify --email test@radziel.com .cli users:create --email test@radziel.com --username radziel --password QWEqwe123 --first Radek --last Gierwialo users:create --mail test@radziel.com --username radziel --password QWEqwe123 --first Radek --last Gierwialo @@ -25,6 +27,4 @@ event . help .exity -.cli events:import:worldsdc --dry-run --limit 20 -exit -.cli events:import:worldsdc --dry-run --limit 20… w CLI REPL \ No newline at end of file +.cli events:import:worldsdc --dry-run --limit 20 \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 7ba313c..6cb4a59 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -7,3 +7,19 @@ @apply bg-gray-50 text-gray-900; } } + +@layer utilities { + @keyframes fade-slide-in { + 0% { + opacity: 0; + transform: translateY(24px) scale(0.96); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } + } + .animate-fade-slide-in { + animation: fade-slide-in 1200ms ease-out both; + } +} diff --git a/frontend/src/pages/EventsPage.jsx b/frontend/src/pages/EventsPage.jsx index 6d94a04..895e62b 100644 --- a/frontend/src/pages/EventsPage.jsx +++ b/frontend/src/pages/EventsPage.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import Layout from '../components/layout/Layout'; import { eventsAPI } from '../services/api'; @@ -9,6 +9,9 @@ const EventsPage = () => { const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + // Pagination controls for revealing older/newer events around the nearest 5 + const [pastShownCount, setPastShownCount] = useState(0); + const [futureShownCount, setFutureShownCount] = useState(0); useEffect(() => { const fetchEvents = async () => { @@ -27,6 +30,61 @@ const EventsPage = () => { fetchEvents(); }, []); + // Build visible list and animation guards BEFORE any conditional returns + const threshold = new Date(); + threshold.setDate(threshold.getDate() - 3); + + const { visibleEvents, pastPool, futurePool, canLoadMorePast, canLoadMoreFuture, initialFutureIds } = useMemo(() => { + const sorted = [...events].sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); + const idxStart = sorted.findIndex((e) => new Date(e.startDate) >= threshold); + const startIdx = idxStart === -1 ? sorted.length : idxStart; + const pastPool = sorted.slice(0, startIdx); + const initialFuture = sorted.slice(startIdx, startIdx + 5); + const futurePool = sorted.slice(startIdx + 5); + + const showPast = pastPool.slice(Math.max(0, pastPool.length - pastShownCount)); + const showFuture = futurePool.slice(0, futureShownCount); + const visibleEvents = [...showPast, ...initialFuture, ...showFuture]; + return { + visibleEvents, + pastPool, + futurePool, + canLoadMorePast: pastShownCount < pastPool.length, + canLoadMoreFuture: futureShownCount < futurePool.length, + initialFutureIds: new Set(initialFuture.map((e) => e.id)), + }; + }, [events, pastShownCount, futureShownCount]); + + // Track previously visible IDs to animate only newly added items + const prevVisibleIdsRef = useRef(new Set()); + const listTopRef = useRef(null); + const isNewById = useMemo(() => { + const map = new Map(); + for (const ev of visibleEvents) { + map.set(ev.id, !prevVisibleIdsRef.current.has(ev.id)); + } + return map; + }, [visibleEvents]); + + useEffect(() => { + prevVisibleIdsRef.current = new Set(visibleEvents.map((e) => e.id)); + }, [visibleEvents]); + + // Preserve scroll position when prepending past items + const handleLoadPrevious = () => { + const containerTop = listTopRef.current?.getBoundingClientRect().top || 0; + setPastShownCount((c) => Math.min(c + 10, pastPool.length)); + requestAnimationFrame(() => { + const newTop = listTopRef.current?.getBoundingClientRect().top || 0; + const delta = newTop - containerTop; + if (delta !== 0) window.scrollBy({ top: delta, left: 0, behavior: 'instant' }); + }); + }; + + const handleLoadLater = () => { + setFutureShownCount((c) => Math.min(c + 10, futurePool.length)); + }; + const handleJoinEvent = (slug) => { navigate(`/events/${slug}/chat`); }; @@ -56,84 +114,122 @@ const EventsPage = () => { ); } + + + // Animated card for smoother reveal of newly mounted items (CSS keyframes) + const EventCard = ({ event, delay = 0, isNew = false, showCheckin = false }) => { + const handleJoinEvent = () => navigate(`/events/${event.slug}/chat`); + return ( +
+
+

{event.name}

+ {event.isJoined && ( + + + Joined + + )} +
+ +
+
+ + {event.location} +
+
+ + + {new Date(event.startDate).toLocaleDateString('en-US')} - {new Date(event.endDate).toLocaleDateString('en-US')} + +
+
+ + {event.participantsCount} participants +
+
+ + {event.description && ( +

{event.description}

+ )} + +
+ {event.isJoined ? ( + + ) : showCheckin ? ( +
+
+ + Check-in required +
+

+ Scan the QR code at the event venue to join the chat +

+
+ ) : null} + + {/* Development mode: Show details link */} + {import.meta.env.DEV && ( + + )} +
+
+ ); + }; + return (

Choose an event

Join an event and start connecting with other dancers

-
- {events.map((event) => ( -
+ +
+ )} -
-
- - {event.location} -
-
- - - {new Date(event.startDate).toLocaleDateString('en-US')} - {new Date(event.endDate).toLocaleDateString('en-US')} - -
-
- - {event.participantsCount} participants -
-
- - {event.description && ( -

{event.description}

- )} - -
- {event.isJoined ? ( - - ) : ( -
-
- - Check-in required -
-

- Scan the QR code at the event venue to join the chat -

-
- )} - - {/* Development mode: Show details link */} - {import.meta.env.DEV && ( - - )} -
-
+
+ {visibleEvents.map((event, idx) => ( + ))}
+ + {canLoadMoreFuture && ( +
+ +
+ )}
);