diff --git a/src/pages/travel/Travel.css b/src/pages/travel/Travel.css index 078ff23..e75d708 100644 --- a/src/pages/travel/Travel.css +++ b/src/pages/travel/Travel.css @@ -71,8 +71,9 @@ } .travel-albums.is-blurred { - filter: blur(3px); - transition: filter 0.2s ease; + opacity: 0.5; + transform: scale(0.995); + transition: opacity 0.2s ease, transform 0.2s ease; } .travel-albums.is-blurred * { @@ -206,6 +207,11 @@ border: 1px solid rgba(255, 255, 255, 0.12); min-height: 220px; cursor: pointer; + opacity: 0; + transform: translateY(22px) scale(0.98); + transition: opacity 0.45s ease, transform 0.45s ease; + transition-delay: var(--reveal-delay, 0ms); + will-change: opacity, transform; } @media (max-width: 768px) { @@ -218,6 +224,11 @@ grid-column: span 2; } +.travel-card[data-revealed='true'] { + opacity: 1; + transform: translateY(0) scale(1); +} + .travel-card img { width: 100%; height: 100%; @@ -247,7 +258,8 @@ .travel-modal { position: fixed; inset: 0; - background: rgba(6, 8, 12, 0.75); + background: rgba(6, 8, 12, 0.55); + backdrop-filter: blur(var(--modal-blur, 6px)); display: grid; align-items: start; justify-items: center; @@ -268,6 +280,17 @@ margin-top: 24px; } +[data-reveal] { + opacity: 0; + transform: translateY(18px); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +[data-reveal][data-revealed='true'] { + opacity: 1; + transform: translateY(0); +} + .travel-modal__summary { display: grid; gap: 6px; @@ -301,6 +324,61 @@ gap: 14px; } +.travel-modal__controls { + display: grid; + gap: 8px; + font-size: 11px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.travel-modal__control { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.travel-blur-slider { + display: flex; + align-items: center; + gap: 10px; +} + +.travel-blur-slider input[type='range'] { + appearance: none; + width: 140px; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 999px; + outline: none; +} + +.travel-blur-slider input[type='range']::-webkit-slider-thumb { + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: #f8f4f0; + border: 1px solid rgba(255, 255, 255, 0.5); + cursor: pointer; +} + +.travel-blur-slider input[type='range']::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: #f8f4f0; + border: 1px solid rgba(255, 255, 255, 0.5); + cursor: pointer; +} + +.travel-blur-value { + font-size: 11px; + color: var(--muted); +} + .travel-modal__frame { width: 100%; height: 68vh; @@ -327,11 +405,20 @@ animation: travel-slide-prev 280ms ease; } +.travel-modal__strip-wrap { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 8px; +} + .travel-modal__strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; + scroll-behavior: smooth; + scrollbar-width: thin; } .travel-modal__thumb { @@ -343,6 +430,7 @@ padding: 0; cursor: pointer; opacity: 0.7; + flex: 0 0 auto; } .travel-modal__thumb img { @@ -358,6 +446,23 @@ border-color: rgba(255, 255, 255, 0.6); } +.travel-modal__strip-arrow { + width: 32px; + height: 32px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(10, 12, 20, 0.7); + color: #f8f4f0; + font-size: 18px; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease; +} + +.travel-modal__strip-arrow:hover { + transform: translateY(-1px); + border-color: rgba(255, 255, 255, 0.5); +} + .travel-modal__meta { margin: 0; color: var(--muted); @@ -393,6 +498,26 @@ transition: transform 0.2s ease, border-color 0.2s ease; } +.travel-modal__arrow.is-loading { + position: relative; +} + +.travel-modal__arrow-icon { + display: block; +} + +.travel-modal__spinner { + position: absolute; + inset: 0; + margin: auto; + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.35); + border-top-color: rgba(255, 255, 255, 0.9); + animation: travel-spin 0.8s linear infinite; +} + .travel-modal__arrow:hover { transform: translateY(-1px) scale(1.02); border-color: rgba(255, 255, 255, 0.5); @@ -423,6 +548,29 @@ } } +@keyframes travel-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.travel-modal__toast { + position: absolute; + left: 24px; + bottom: 20px; + padding: 8px 12px; + border-radius: 999px; + background: rgba(10, 12, 20, 0.85); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #f8f4f0; + font-size: 12px; + letter-spacing: 0.04em; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3); +} + @media (max-width: 768px) { .travel-modal__content { padding: 16px; @@ -476,3 +624,12 @@ height: 160px; } } + +@media (prefers-reduced-motion: reduce) { + .travel-card, + [data-reveal] { + opacity: 1 !important; + transform: none !important; + transition: none !important; + } +} diff --git a/src/pages/travel/Travel.jsx b/src/pages/travel/Travel.jsx index 4622e81..f5c348a 100644 --- a/src/pages/travel/Travel.jsx +++ b/src/pages/travel/Travel.jsx @@ -56,6 +56,8 @@ const TravelPhotoGrid = ({ isLoadingMore, }) => { const sentinelRef = useRef(null); + const gridRef = useRef(null); + const revealObserverRef = useRef(null); useEffect(() => { const sentinel = sentinelRef.current; @@ -74,9 +76,48 @@ const TravelPhotoGrid = ({ return () => observer.disconnect(); }, [hasNext, isLoadingMore, onLoadMore]); + useEffect(() => { + revealObserverRef.current = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (!entry.isIntersecting) return; + entry.target.dataset.revealed = 'true'; + revealObserverRef.current?.unobserve(entry.target); + }); + }, + { rootMargin: '120px', threshold: 0.15 } + ); + + return () => revealObserverRef.current?.disconnect(); + }, []); + + useEffect(() => { + const observer = revealObserverRef.current; + const grid = gridRef.current; + if (!observer || !grid) return; + const cards = grid.querySelectorAll( + '.travel-card:not([data-revealed="true"])' + ); + cards.forEach((card) => observer.observe(card)); + const fallback = window.setTimeout(() => { + const stillHidden = grid.querySelectorAll( + '.travel-card:not([data-revealed="true"])' + ); + if (stillHidden.length) { + stillHidden.forEach( + (card) => (card.dataset.revealed = 'true') + ); + } + }, 500); + return () => { + window.clearTimeout(fallback); + cards.forEach((card) => observer.unobserve(card)); + }; + }, [photos.length]); + return ( <> -
Visual Diary
Select a region
{selectedRegion @@ -557,7 +731,10 @@ const Travel = () => { role="dialog" aria-modal="true" onClick={() => setSelectedPhotoIndex(null)} - style={{ '--modal-offset': `${modalOffset}px` }} + style={{ + '--modal-offset': `${modalOffset}px`, + '--modal-blur': `${backdropBlur}px`, + }} >
@@ -677,6 +949,11 @@ const Travel = () => { : ''}
) : null} + {toastMessage ? ( +