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.description}
+ )} + ++ Scan the QR code at the event venue to join the chat +
+Join an event and start connecting with other dancers
-{event.description}
- )} - -- Scan the QR code at the event venue to join the chat -
-