feat: migrate app to local Fastify and Postgres stack
Replace Supabase auth and search runtime with a local Fastify API, PostgreSQL/PostGIS schema, and local session handling. Scaffold the worker and deep-research foundations while keeping the existing research, dashboard, and map flows running on the new backend.
This commit is contained in:
+92
-90
@@ -1,21 +1,22 @@
|
||||
import { type ReactElement, type SVGProps, useEffect, useState } from 'react';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { APIProvider } from '@vis.gl/react-google-maps';
|
||||
import { AlertCircle, Briefcase, LogIn, ShieldAlert, UserPlus } from 'lucide-react';
|
||||
import { Layout } from './components/Layout';
|
||||
import { SearchSetup } from './components/SearchSetup';
|
||||
import { Dashboard } from './components/Dashboard';
|
||||
import { MapView } from './components/MapView';
|
||||
import { hasSupabaseConfig, supabase } from './lib/supabase';
|
||||
import type { AppUser } from '../shared/types';
|
||||
import { getLocalSessionUser, signInWithLocalAuth, signOutWithLocalAuth, signUpWithLocalAuth } from './lib/auth';
|
||||
import { hasApiConfig } from './lib/api';
|
||||
|
||||
const GOOGLE_MAPS_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_PLATFORM_KEY ?? '';
|
||||
const hasValidMapsKey = Boolean(GOOGLE_MAPS_API_KEY) && GOOGLE_MAPS_API_KEY !== 'YOUR_API_KEY';
|
||||
|
||||
export default function App() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [user, setUser] = useState<AppUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'setup' | 'dashboard' | 'map'>('setup');
|
||||
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
||||
const [selectedJobIds, setSelectedJobIds] = useState<string[]>([]);
|
||||
const [authError, setAuthError] = useState<string | null>(null);
|
||||
const [authNotice, setAuthNotice] = useState<string | null>(null);
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
@@ -28,99 +29,96 @@ export default function App() {
|
||||
let isMounted = true;
|
||||
|
||||
const loadSession = async () => {
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
try {
|
||||
const sessionUser = await getLocalSessionUser();
|
||||
|
||||
if (!isMounted) {
|
||||
return;
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(sessionUser);
|
||||
} catch (error) {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAuthError(error instanceof Error ? error.message : 'Failed to load session.');
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
}
|
||||
|
||||
setUser(data.session?.user ?? null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
void loadSession();
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSelectJob = (jobId: string) => {
|
||||
setSelectedJobId(jobId);
|
||||
const handleToggleJobSelection = (jobId: string) => {
|
||||
setSelectedJobIds((currentJobIds) =>
|
||||
currentJobIds.includes(jobId) ? currentJobIds.filter((currentJobId) => currentJobId !== jobId) : [...currentJobIds, jobId],
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectCreatedJob = (jobId: string) => {
|
||||
setSelectedJobIds((currentJobIds) => (currentJobIds.includes(jobId) ? currentJobIds : [...currentJobIds, jobId]));
|
||||
};
|
||||
|
||||
const handleShowSelectedOnMap = () => {
|
||||
if (selectedJobIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTab('map');
|
||||
};
|
||||
|
||||
const handleClearSelectedJobs = () => {
|
||||
setSelectedJobIds([]);
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
setAuthError(null);
|
||||
setAuthNotice(null);
|
||||
setIsAuthenticating(true);
|
||||
|
||||
if (authMode === 'sign_up') {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
full_name: displayName.trim() || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
if (authMode === 'sign_up') {
|
||||
const nextUser = await signUpWithLocalAuth({
|
||||
email,
|
||||
password,
|
||||
displayName: displayName.trim() || undefined,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
setIsAuthenticating(false);
|
||||
setUser(nextUser);
|
||||
setAuthNotice('Account created and signed in.');
|
||||
return;
|
||||
}
|
||||
|
||||
setAuthNotice(
|
||||
data.session
|
||||
? 'Account created and signed in.'
|
||||
: 'Account created. If email confirmation is enabled, check your inbox before signing in.',
|
||||
);
|
||||
const nextUser = await signInWithLocalAuth({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
setUser(nextUser);
|
||||
} catch (error) {
|
||||
setAuthError(error instanceof Error ? error.message : 'Authentication failed.');
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
setIsAuthenticating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAuthenticating(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
return;
|
||||
try {
|
||||
await signOutWithLocalAuth();
|
||||
setUser(null);
|
||||
setSelectedJobIds([]);
|
||||
setAuthNotice(null);
|
||||
} catch (error) {
|
||||
setAuthError(error instanceof Error ? error.message : 'Failed to sign out.');
|
||||
}
|
||||
|
||||
setSelectedJobId(null);
|
||||
setAuthNotice(null);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -131,33 +129,33 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasSupabaseConfig) {
|
||||
if (!hasApiConfig) {
|
||||
return (
|
||||
<ConfigScreen
|
||||
icon={<ShieldAlert className="h-8 w-8" />}
|
||||
title="Supabase Config Required"
|
||||
description="Add your Supabase project URL and anon key before running the app."
|
||||
title="Local API Config Required"
|
||||
description="Add your local API base URL before running the app."
|
||||
steps={[
|
||||
'Create a Supabase project.',
|
||||
'Add VITE_SUPABASE_URL to your local environment.',
|
||||
'Add VITE_SUPABASE_ANON_KEY to your local environment.',
|
||||
'Start the local Fastify API server.',
|
||||
'Add VITE_API_BASE_URL to your local environment.',
|
||||
'Ensure the API can reach your local PostgreSQL database.',
|
||||
'Restart the Vite dev server after updating env vars.',
|
||||
]}
|
||||
footer="The app uses Supabase Auth, Postgres, and Edge Functions now."
|
||||
footer="The app now uses a local API, PostgreSQL, and local session auth."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasValidMapsKey) {
|
||||
return (
|
||||
<ConfigScreen
|
||||
icon={<MapIcon className="h-8 w-8" />}
|
||||
title="Google Maps API Key Required"
|
||||
description="Add a browser key for map rendering and a server key for the Supabase search function."
|
||||
<ConfigScreen
|
||||
icon={<MapIcon className="h-8 w-8" />}
|
||||
title="Google Maps API Key Required"
|
||||
description="Add a browser key for map rendering and a server key for the local search API."
|
||||
steps={[
|
||||
'Create a Google Maps Platform API key for the browser app.',
|
||||
'Set VITE_GOOGLE_MAPS_PLATFORM_KEY locally for the frontend.',
|
||||
'Set GOOGLE_MAPS_SERVER_KEY in Supabase Edge Function secrets.',
|
||||
'Set GOOGLE_MAPS_SERVER_KEY for the local API server.',
|
||||
'Enable Geocoding API and Places API in Google Cloud.',
|
||||
]}
|
||||
footer="The app will start once the browser key is available."
|
||||
@@ -173,8 +171,8 @@ export default function App() {
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 text-emerald-600">
|
||||
<Briefcase className="h-8 w-8" />
|
||||
</div>
|
||||
<h1 className="mt-6 text-3xl font-bold tracking-tight text-stone-900">Lead Finder</h1>
|
||||
<p className="mt-2 text-stone-600">Use Supabase email auth to access your lead workspace.</p>
|
||||
<h1 className="mt-6 text-3xl font-bold tracking-tight text-stone-900">Leads4less</h1>
|
||||
<p className="mt-2 text-stone-600">Create a local account to access your lead workspace.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 rounded-xl bg-stone-100 p-1">
|
||||
@@ -298,17 +296,21 @@ export default function App() {
|
||||
<Layout
|
||||
user={user}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={(tab) => {
|
||||
setActiveTab(tab);
|
||||
if (tab !== 'map') {
|
||||
setSelectedJobId(null);
|
||||
}
|
||||
}}
|
||||
setActiveTab={setActiveTab}
|
||||
onLogout={() => void handleLogout()}
|
||||
>
|
||||
{activeTab === 'setup' && <SearchSetup user={user} onSelectJob={handleSelectJob} />}
|
||||
{activeTab === 'setup' && (
|
||||
<SearchSetup
|
||||
user={user}
|
||||
selectedJobIds={selectedJobIds}
|
||||
onToggleJobSelection={handleToggleJobSelection}
|
||||
onSelectCreatedJob={handleSelectCreatedJob}
|
||||
onShowSelectedOnMap={handleShowSelectedOnMap}
|
||||
onClearSelection={handleClearSelectedJobs}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'dashboard' && <Dashboard user={user} />}
|
||||
{activeTab === 'map' && <MapView user={user} jobId={selectedJobId} />}
|
||||
{activeTab === 'map' && <MapView user={user} jobIds={selectedJobIds} />}
|
||||
</Layout>
|
||||
</APIProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user