diff --git a/src/pages/travel/Travel.css b/src/pages/travel/Travel.css index 40600b2..0c7ce1f 100644 --- a/src/pages/travel/Travel.css +++ b/src/pages/travel/Travel.css @@ -92,6 +92,64 @@ max-width: 420px; } +/* Sync button */ +.tv-sync-btn { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 14px; + padding: 6px 14px; + background: var(--tv-surface-2); + border: 1px solid var(--tv-line); + border-radius: var(--tv-r-sm); + color: var(--tv-muted); + font-family: var(--tv-mono); + font-size: 11px; + cursor: pointer; + transition: color 0.2s, border-color 0.2s; +} + +.tv-sync-btn:hover:not(:disabled) { + color: var(--tv-text); + border-color: var(--tv-line-bright); +} + +.tv-sync-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.tv-sync-btn.is-syncing svg { + animation: tv-spin 1s linear infinite; +} + +@keyframes tv-spin { + to { transform: rotate(360deg); } +} + +.tv-sync-toast { + margin-top: 8px; + padding: 6px 12px; + background: rgba(200, 144, 94, 0.12); + border: 1px solid rgba(200, 144, 94, 0.25); + border-radius: var(--tv-r-sm); + font-family: var(--tv-mono); + font-size: 11px; + color: var(--tv-accent); + animation: tv-fade-in 0.3s ease; +} + +.tv-sync-toast.is-error { + background: rgba(220, 80, 80, 0.12); + border-color: rgba(220, 80, 80, 0.25); + color: #dc5050; +} + +@keyframes tv-fade-in { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} + /* Active region info */ .tv-header__active-region { display: flex; diff --git a/src/pages/travel/Travel.jsx b/src/pages/travel/Travel.jsx index 47c507f..2b2eda2 100644 --- a/src/pages/travel/Travel.jsx +++ b/src/pages/travel/Travel.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import useTravelData from './useTravelData'; import MiniMap, { getRegionAccent } from './MiniMap'; import AlbumCard from './AlbumCard'; @@ -30,6 +30,9 @@ const Travel = () => { /* ── Local state ──────────────────────────── */ const [selectedAlbum, setSelectedAlbum] = useState(null); const [albumSourceRect, setAlbumSourceRect] = useState(null); + const [syncing, setSyncing] = useState(false); + const [syncResult, setSyncResult] = useState(null); + const syncTimerRef = useRef(null); /* ── Computed ──────────────────────────────── */ const regionAccent = getRegionAccent(selectedRegion || ''); @@ -71,6 +74,26 @@ const Travel = () => { return reloadAlbumPhotos(selectedAlbum.region, selectedAlbum.name); }, [selectedAlbum, reloadAlbumPhotos]); + const handleSync = useCallback(async () => { + if (syncing) return; + setSyncing(true); + setSyncResult(null); + try { + const res = await fetch('/api/travel/sync', { method: 'POST' }); + if (!res.ok) throw new Error(`${res.status}`); + const data = await res.json(); + setSyncResult(data); + if (syncTimerRef.current) clearTimeout(syncTimerRef.current); + syncTimerRef.current = setTimeout(() => setSyncResult(null), 8000); + } catch (e) { + setSyncResult({ error: e.message }); + if (syncTimerRef.current) clearTimeout(syncTimerRef.current); + syncTimerRef.current = setTimeout(() => setSyncResult(null), 5000); + } finally { + setSyncing(false); + } + }, [syncing]); + /* ── Render ────────────────────────────────── */ return (