Quick save
This commit is contained in:
parent
e5d5819098
commit
2906b01734
25 changed files with 1070 additions and 237 deletions
|
|
@ -4,14 +4,17 @@ import MapPage, { type ExportState } from './components/map/MapPage';
|
|||
import DataSourcesPage from './components/data-sources/DataSourcesPage';
|
||||
import FAQPage from './components/faq/FAQPage';
|
||||
import HomePage from './components/home/HomePage';
|
||||
import SavedSearchesPage from './components/saved-searches/SavedSearchesPage';
|
||||
import Header, { type Page } from './components/ui/Header';
|
||||
import AuthModal from './components/ui/AuthModal';
|
||||
import SaveSearchModal from './components/ui/SaveSearchModal';
|
||||
import type { FeatureMeta, FeatureGroup, POICategoriesResponse, POICategoryGroup } from './types';
|
||||
import { fetchWithRetry, apiUrl } from './lib/api';
|
||||
import { parseUrlState } from './lib/url-state';
|
||||
import { INITIAL_VIEW_STATE } from './lib/consts';
|
||||
import { useTheme } from './hooks/useTheme';
|
||||
import { useAuth } from './hooks/useAuth';
|
||||
import { useSavedSearches } from './hooks/useSavedSearches';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -19,6 +22,25 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
function pageToPath(page: Page): string {
|
||||
switch (page) {
|
||||
case 'dashboard': return '/dashboard';
|
||||
case 'data-sources': return '/data-sources';
|
||||
case 'faq': return '/faq';
|
||||
case 'saved-searches': return '/saved';
|
||||
default: return '/';
|
||||
}
|
||||
}
|
||||
|
||||
function pathToPage(pathname: string): Page | null {
|
||||
if (pathname === '/dashboard') return 'dashboard';
|
||||
if (pathname === '/data-sources') return 'data-sources';
|
||||
if (pathname === '/faq') return 'faq';
|
||||
if (pathname === '/saved') return 'saved-searches';
|
||||
if (pathname === '/') return 'home';
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const urlState = useMemo(() => parseUrlState(), []);
|
||||
const initialViewState = useMemo(() => urlState.viewState || INITIAL_VIEW_STATE, []);
|
||||
|
|
@ -27,6 +49,10 @@ export default function App() {
|
|||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('screenshot') === '1';
|
||||
}, []);
|
||||
const isOgMode = useMemo(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('og') === '1';
|
||||
}, []);
|
||||
|
||||
// Core data
|
||||
const [features, setFeatures] = useState<FeatureMeta[]>([]);
|
||||
|
|
@ -37,11 +63,27 @@ export default function App() {
|
|||
const [pendingInfoFeature, setPendingInfoFeature] = useState<string | null>(null);
|
||||
const [activePage, setActivePage] = useState<Page>(() => {
|
||||
if (isScreenshotMode) return 'dashboard';
|
||||
|
||||
// Derive page from URL pathname
|
||||
const fromPath = pathToPage(window.location.pathname);
|
||||
if (fromPath) return fromPath;
|
||||
|
||||
// Restore from history state (e.g. popstate)
|
||||
if (window.history.state?.page) return window.history.state.page;
|
||||
|
||||
// Backward compat: dashboard params on unknown path
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.has('v') || params.has('f') || params.has('poi') || params.has('tab')
|
||||
? 'dashboard'
|
||||
: 'home';
|
||||
if (params.has('v') || params.has('f') || params.has('poi') || params.has('tab')) {
|
||||
// Rewrite URL to /dashboard keeping query params
|
||||
window.history.replaceState(
|
||||
{ page: 'dashboard' },
|
||||
'',
|
||||
`/dashboard${window.location.search}`
|
||||
);
|
||||
return 'dashboard';
|
||||
}
|
||||
|
||||
return 'home';
|
||||
});
|
||||
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
|
@ -52,9 +94,14 @@ export default function App() {
|
|||
login,
|
||||
register,
|
||||
logout,
|
||||
requestPasswordReset,
|
||||
clearError,
|
||||
} = useAuth();
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [authModalTab, setAuthModalTab] = useState<'login' | 'register'>('login');
|
||||
|
||||
const savedSearches = useSavedSearches(user?.id ?? null);
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
|
||||
// Load features and POI categories on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -92,21 +139,15 @@ export default function App() {
|
|||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
// Screenshot mode ready signal
|
||||
useEffect(() => {
|
||||
if (isScreenshotMode && !initialLoading && features.length > 0) {
|
||||
window.__og_ready = true;
|
||||
}
|
||||
}, [isScreenshotMode, initialLoading, features]);
|
||||
// Screenshot mode ready signal — MapPage sets __og_ready once map data loads
|
||||
|
||||
// Navigation
|
||||
const navigateTo = useCallback((page: Page, hash?: string, infoFeature?: string) => {
|
||||
if (infoFeature) {
|
||||
window.history.replaceState({ ...window.history.state, infoFeature }, '');
|
||||
}
|
||||
const url = hash
|
||||
? `${window.location.pathname}${window.location.search}#${hash}`
|
||||
: `${window.location.pathname}${window.location.search}`;
|
||||
const path = pageToPath(page);
|
||||
const url = hash ? `${path}#${hash}` : path;
|
||||
window.history.pushState({ page }, '', url);
|
||||
setActivePage(page);
|
||||
trackPageview();
|
||||
|
|
@ -114,7 +155,11 @@ export default function App() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!window.history.state?.page) {
|
||||
window.history.replaceState({ page: activePage }, '');
|
||||
window.history.replaceState(
|
||||
{ page: activePage },
|
||||
'',
|
||||
pageToPath(activePage) + window.location.search + window.location.hash
|
||||
);
|
||||
}
|
||||
const handlePopState = (e: PopStateEvent) => {
|
||||
if (e.state?.page) {
|
||||
|
|
@ -122,12 +167,23 @@ export default function App() {
|
|||
if (e.state.infoFeature) {
|
||||
setPendingInfoFeature(e.state.infoFeature);
|
||||
}
|
||||
} else {
|
||||
// Fall back to deriving page from pathname
|
||||
const page = pathToPage(window.location.pathname);
|
||||
setActivePage(page || 'home');
|
||||
}
|
||||
};
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Fetch saved searches when page becomes active
|
||||
useEffect(() => {
|
||||
if (activePage === 'saved-searches') {
|
||||
savedSearches.fetchSearches();
|
||||
}
|
||||
}, [activePage, savedSearches.fetchSearches]);
|
||||
|
||||
const [exportState, setExportState] = useState<ExportState | null>(null);
|
||||
|
||||
if (isScreenshotMode) {
|
||||
|
|
@ -145,6 +201,7 @@ export default function App() {
|
|||
onClearPendingInfoFeature={() => {}}
|
||||
onNavigateTo={() => {}}
|
||||
screenshotMode
|
||||
ogMode={isOgMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -158,8 +215,17 @@ export default function App() {
|
|||
onToggleTheme={toggleTheme}
|
||||
onExport={exportState?.onExport ?? null}
|
||||
exporting={exportState?.exporting ?? false}
|
||||
onSaveSearch={activePage === 'dashboard' && user ? () => setShowSaveModal(true) : null}
|
||||
savingSearch={savedSearches.saving}
|
||||
user={user}
|
||||
onLoginClick={() => setShowAuthModal(true)}
|
||||
onLoginClick={() => {
|
||||
setAuthModalTab('login');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onRegisterClick={() => {
|
||||
setAuthModalTab('register');
|
||||
setShowAuthModal(true);
|
||||
}}
|
||||
onLogout={logout}
|
||||
/>
|
||||
{activePage === 'home' ? (
|
||||
|
|
@ -168,6 +234,15 @@ export default function App() {
|
|||
<DataSourcesPage />
|
||||
) : activePage === 'faq' ? (
|
||||
<FAQPage />
|
||||
) : activePage === 'saved-searches' ? (
|
||||
<SavedSearchesPage
|
||||
searches={savedSearches.searches}
|
||||
loading={savedSearches.loading}
|
||||
onDelete={savedSearches.deleteSearch}
|
||||
onOpen={(params) => {
|
||||
window.location.href = `/?${params}`;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MapPage
|
||||
features={features}
|
||||
|
|
@ -189,9 +264,19 @@ export default function App() {
|
|||
onClose={() => setShowAuthModal(false)}
|
||||
onLogin={login}
|
||||
onRegister={register}
|
||||
onForgotPassword={requestPasswordReset}
|
||||
loading={authLoading}
|
||||
error={authError}
|
||||
onClearError={clearError}
|
||||
initialTab={authModalTab}
|
||||
/>
|
||||
)}
|
||||
{showSaveModal && (
|
||||
<SaveSearchModal
|
||||
onClose={() => setShowSaveModal(false)}
|
||||
onSave={savedSearches.saveSearch}
|
||||
saving={savedSearches.saving}
|
||||
error={savedSearches.error}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue