Public Access
1
0

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:
pguerrerox
2026-03-27 13:56:54 +00:00
parent 0e4910805a
commit a1ba5ee093
44 changed files with 3756 additions and 1128 deletions
+92 -90
View File
@@ -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>
);