123 lines
3.5 KiB
JavaScript
123 lines
3.5 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Sun, Moon } from 'lucide-react';
|
|
import Timeline from './components/Timeline';
|
|
import PhotoDetail from './components/PhotoDetail';
|
|
import Admin from './components/Admin';
|
|
import { fetchPhotos } from './data/photos';
|
|
|
|
function App() {
|
|
const [view, setView] = useState(() => {
|
|
return window.location.pathname === '/admin' ? 'admin' : 'timeline';
|
|
});
|
|
|
|
const [activePhoto, setActivePhoto] = useState(null);
|
|
const [photos, setPhotos] = useState([]);
|
|
|
|
useEffect(() => {
|
|
document.title = import.meta.env.VITE_APP_TITLE || 'Chronicle';
|
|
}, []);
|
|
|
|
// Theme State
|
|
const [theme, setTheme] = useState(() => {
|
|
return localStorage.getItem('theme_preference') || 'dark';
|
|
});
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme_preference', theme);
|
|
}, [theme]);
|
|
|
|
const toggleTheme = () => {
|
|
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
|
|
};
|
|
|
|
const loadPhotos = async () => {
|
|
const data = await fetchPhotos();
|
|
setPhotos(data);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const fetchAndSet = async () => {
|
|
const data = await fetchPhotos();
|
|
setPhotos(data);
|
|
};
|
|
fetchAndSet();
|
|
}, [view]);
|
|
|
|
// Handle browser back/forward
|
|
useEffect(() => {
|
|
const handlePopState = () => {
|
|
setView(window.location.pathname === '/admin' ? 'admin' : 'timeline');
|
|
};
|
|
window.addEventListener('popstate', handlePopState);
|
|
return () => window.removeEventListener('popstate', handlePopState);
|
|
}, []);
|
|
|
|
// Update URL when view changes
|
|
useEffect(() => {
|
|
const path = view === 'admin' ? '/admin' : '/';
|
|
if (window.location.pathname !== path) {
|
|
window.history.pushState({}, '', path);
|
|
}
|
|
}, [view]);
|
|
|
|
if (view === 'admin') {
|
|
return <Admin onBack={() => setView('timeline')} onUpdate={loadPhotos} />;
|
|
}
|
|
|
|
return (
|
|
<div className="app-container" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
{/* Theme Toggle */}
|
|
<div style={{ position: 'fixed', top: '20px', right: '20px', zIndex: 50 }}>
|
|
<button
|
|
onClick={toggleTheme}
|
|
style={{
|
|
background: 'transparent',
|
|
color: 'var(--text-primary)',
|
|
padding: '8px',
|
|
borderRadius: '50%',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
opacity: 0.7,
|
|
transition: 'opacity 0.2s'
|
|
}}
|
|
onMouseEnter={(e) => e.currentTarget.style.opacity = '1'}
|
|
onMouseLeave={(e) => e.currentTarget.style.opacity = '0.7'}
|
|
>
|
|
{theme === 'dark' ? <Sun size={20} color="white" /> : <Moon size={20} color="black" />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Main Timeline View */}
|
|
<div style={{ flex: 1 }}>
|
|
<Timeline photos={photos} onSelectPhoto={setActivePhoto} />
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<footer style={{
|
|
padding: '2rem',
|
|
textAlign: 'center',
|
|
color: 'var(--text-secondary)',
|
|
fontSize: '0.9rem',
|
|
borderTop: '1px solid var(--border)',
|
|
marginTop: 'auto'
|
|
}}>
|
|
<p>© {new Date().getFullYear()} {import.meta.env.VITE_APP_TITLE || 'Chronicle'}. All rights reserved.</p>
|
|
</footer>
|
|
|
|
{/* Detail Overlay */}
|
|
{activePhoto && (
|
|
<PhotoDetail
|
|
photo={activePhoto}
|
|
onClose={() => setActivePhoto(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|